From 5fe5fd71b5058767cb8346cc10bccd2dc127f032 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Mon, 21 Dec 2020 16:19:40 -0500 Subject: [PATCH 01/28] PRVB --- 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 3a6dc473f..987452255 100644 --- a/netbox/netbox/settings.py +++ b/netbox/netbox/settings.py @@ -16,7 +16,7 @@ from django.core.validators import URLValidator # Environment setup # -VERSION = '2.10.2' +VERSION = '2.10.3-dev' # Hostname HOSTNAME = platform.node() From 8f4197c0207dd9467dc09b8e89dfcd896b2c8a79 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Tue, 22 Dec 2020 10:28:09 -0500 Subject: [PATCH 02/28] Fixes #5518: Fix persistent vertical scrollbar --- docs/release-notes/version-2.10.md | 8 ++++++++ netbox/project-static/css/base.css | 6 +++--- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/docs/release-notes/version-2.10.md b/docs/release-notes/version-2.10.md index 27965090b..91a65fbfe 100644 --- a/docs/release-notes/version-2.10.md +++ b/docs/release-notes/version-2.10.md @@ -1,5 +1,13 @@ # NetBox v2.10 +## v2.10.3 (FUTURE) + +### Bug Fixes + +* [#5518](https://github.com/netbox-community/netbox/issues/5518) - Fix persistent vertical scrollbar + +--- + ## v2.10.2 (2020-12-21) ### Enhancements diff --git a/netbox/project-static/css/base.css b/netbox/project-static/css/base.css index 681565b9e..94bd74a46 100644 --- a/netbox/project-static/css/base.css +++ b/netbox/project-static/css/base.css @@ -14,21 +14,21 @@ body { .wrapper { min-height: 100%; height: auto !important; - margin: 0 auto -61px; /* the bottom margin is the negative value of the footer's height */ + margin: 0 auto -48px; /* the bottom margin is the negative value of the footer's height */ padding-bottom: 30px; } .navbar-brand { padding: 12px 15px 8px; } .footer, .push { - height: 60px; /* .push must be the same height as .footer */ + height: 48px; /* .push must be the same height as .footer */ } .footer { background-color: #f5f5f5; border-top: 1px solid #d0d0d0; } footer p { - margin: 20px 0; + margin: 12px 0; } #navbar_search { padding: 0 8px; From dc6dbdf3c4ed42470b3b62bc00da621b57d5d10e Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Tue, 22 Dec 2020 13:12:38 -0500 Subject: [PATCH 03/28] Update documentation to reference the feature branch --- docs/development/getting-started.md | 6 +++--- docs/development/release-checklist.md | 5 +---- docs/installation/3-netbox.md | 2 +- 3 files changed, 5 insertions(+), 8 deletions(-) diff --git a/docs/development/getting-started.md b/docs/development/getting-started.md index 8745825ab..9b2249653 100644 --- a/docs/development/getting-started.md +++ b/docs/development/getting-started.md @@ -27,13 +27,13 @@ base_requirements.txt contrib docs mkdocs.yml NOTICE requ CHANGELOG.md CONTRIBUTING.md LICENSE.txt netbox README.md scripts ``` -The NetBox project utilizes three long-term branches: +The NetBox project utilizes three persistent git branches to track work: * `master` - Serves as a snapshot of the current stable release * `develop` - All development on the upcoming stable release occurs here -* `develop-x.y` - Tracks work on an upcoming major release +* `feature` - Tracks work on an upcoming major release -Typically, you'll base pull requests off of the `develop` branch, or off of `develop-x.y` if you're working on a new major release. **Never** base pull requests off of the master branch, which receives merged only from the `develop` branch. +Typically, you'll base pull requests off of the `develop` branch, or off of `feature` if you're working on a new major release. **Never** merge pull requests into the `master` branch, which receives merged only from the `develop` branch. ### Enable Pre-Commit Hooks diff --git a/docs/development/release-checklist.md b/docs/development/release-checklist.md index 22fa6f1e8..f3338ffd3 100644 --- a/docs/development/release-checklist.md +++ b/docs/development/release-checklist.md @@ -52,10 +52,7 @@ Close the release milestone on GitHub after ensuring there are no remaining open ### Merge the Release Branch -Submit a pull request to merge the release branch `develop-x.y` into the `develop` branch in preparation for its releases. - -!!! warning - No further releases for the current major version can be published once this pull request is merged. +Submit a pull request to merge the `feature` branch into the `develop` branch in preparation for its release. --- diff --git a/docs/installation/3-netbox.md b/docs/installation/3-netbox.md index 39189bbd1..9745df1f3 100644 --- a/docs/installation/3-netbox.md +++ b/docs/installation/3-netbox.md @@ -83,7 +83,7 @@ Checking connectivity... done. ``` !!! note - Installation via git also allows you to easily try out development versions of NetBox. The `develop` branch contains all work underway for the next minor release, and the `develop-x.y` branch (if present) tracks progress on the next major release. + Installation via git also allows you to easily try out development versions of NetBox. The `develop` branch contains all work underway for the next minor release, and the `feature` branch tracks progress on the next major release. ## Create the NetBox System User From 09633ee11b8e9974e12a5155656c8b8fb0ed0871 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Tue, 22 Dec 2020 13:51:58 -0500 Subject: [PATCH 04/28] Move rack group field directly beneath site --- netbox/templates/dcim/rack_edit.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/netbox/templates/dcim/rack_edit.html b/netbox/templates/dcim/rack_edit.html index 94456ac73..9ce242191 100644 --- a/netbox/templates/dcim/rack_edit.html +++ b/netbox/templates/dcim/rack_edit.html @@ -7,9 +7,9 @@
{% render_field form.region %} {% render_field form.site %} + {% render_field form.group %} {% render_field form.name %} {% render_field form.facility_id %} - {% render_field form.group %} {% render_field form.status %} {% render_field form.role %} {% render_field form.serial %} From e4f22bc4941ecea0e3828c71b29ac91bfe213f65 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Tue, 22 Dec 2020 15:22:53 -0500 Subject: [PATCH 05/28] Employ signals to update child objects when RackGroup/Rack site assignment changes --- netbox/dcim/models/racks.py | 16 --------- netbox/dcim/signals.py | 42 +++++++++++++++++++++- netbox/dcim/tests/test_models.py | 61 ++++++++++++++++++++++++++++++++ 3 files changed, 102 insertions(+), 17 deletions(-) diff --git a/netbox/dcim/models/racks.py b/netbox/dcim/models/racks.py index cad20241b..de9021f35 100644 --- a/netbox/dcim/models/racks.py +++ b/netbox/dcim/models/racks.py @@ -326,22 +326,6 @@ class Rack(ChangeLoggedModel, CustomFieldModel): 'group': "Rack group must be from the same site, {}.".format(self.site) }) - def save(self, *args, **kwargs): - - # Record the original site assignment for this rack. - _site_id = None - if self.pk: - _site_id = Rack.objects.get(pk=self.pk).site_id - - super().save(*args, **kwargs) - - # Update racked devices if the assigned Site has been changed. - if _site_id is not None and self.site_id != _site_id: - devices = Device.objects.filter(rack=self) - for device in devices: - device.site = self.site - device.save() - def to_csv(self): return ( self.site.name, diff --git a/netbox/dcim/signals.py b/netbox/dcim/signals.py index 33c4b461c..380c0aec0 100644 --- a/netbox/dcim/signals.py +++ b/netbox/dcim/signals.py @@ -2,12 +2,13 @@ import logging from cacheops import invalidate_obj from django.contrib.contenttypes.models import ContentType +from django.db.models import Q from django.db.models.signals import post_save, post_delete, pre_delete from django.db import transaction from django.dispatch import receiver from .choices import CableStatusChoices -from .models import Cable, CablePath, Device, PathEndpoint, VirtualChassis +from .models import Cable, CablePath, Device, PathEndpoint, Rack, RackGroup, VirtualChassis def create_cablepath(node): @@ -36,6 +37,40 @@ def rebuild_paths(obj): create_cablepath(cp.origin) +# +# Site/rack/device assignment +# + +@receiver(post_save, sender=RackGroup) +def handle_rackgroup_site_change(instance, created, **kwargs): + """ + Update child RackGroups and Racks if Site assignment has changed. We intentionally recurse through each child + object instead of calling update() on the QuerySet to ensure the proper change records get created for each. + """ + if not created: + for rackgroup in instance.get_children(): + rackgroup.site = instance.site + rackgroup.save() + for rack in Rack.objects.filter(group=instance).exclude(site=instance.site): + rack.site = instance.site + rack.save() + + +@receiver(post_save, sender=Rack) +def handle_rack_site_change(instance, created, **kwargs): + """ + Update child Devices if Site assignment has changed. + """ + if not created: + for device in Device.objects.filter(rack=instance).exclude(site=instance.site): + device.site = instance.site + device.save() + + +# +# Virtual chassis +# + @receiver(post_save, sender=VirtualChassis) def assign_virtualchassis_master(instance, created, **kwargs): """ @@ -60,6 +95,11 @@ def clear_virtualchassis_members(instance, **kwargs): device.save() +# +# Cables +# + + @receiver(post_save, sender=Cable) def update_connected_endpoints(instance, created, raw=False, **kwargs): """ diff --git a/netbox/dcim/tests/test_models.py b/netbox/dcim/tests/test_models.py index b20e21102..a372bb099 100644 --- a/netbox/dcim/tests/test_models.py +++ b/netbox/dcim/tests/test_models.py @@ -7,6 +7,39 @@ from dcim.models import * from tenancy.models import Tenant +class RackGroupTestCase(TestCase): + + def test_change_rackgroup_site(self): + """ + Check that all child RackGroups and Racks get updated when a RackGroup is moved to a new Site. Topology: + Site A + - RackGroup A1 + - RackGroup A2 + - Rack 2 + - Rack 1 + """ + site_a = Site.objects.create(name='Site A', slug='site-a') + site_b = Site.objects.create(name='Site B', slug='site-b') + + rackgroup_a1 = RackGroup(site=site_a, name='RackGroup A1', slug='rackgroup-a1') + rackgroup_a1.save() + rackgroup_a2 = RackGroup(site=site_a, parent=rackgroup_a1, name='RackGroup A2', slug='rackgroup-a2') + rackgroup_a2.save() + + rack1 = Rack.objects.create(site=site_a, group=rackgroup_a1, name='Rack 1') + rack2 = Rack.objects.create(site=site_a, group=rackgroup_a2, name='Rack 2') + + # Move RackGroup A1 to Site B + rackgroup_a1.site = site_b + rackgroup_a1.save() + + # Check that all objects within RackGroup A1 now belong to Site B + self.assertEqual(RackGroup.objects.get(pk=rackgroup_a1.pk).site, site_b) + self.assertEqual(RackGroup.objects.get(pk=rackgroup_a2.pk).site, site_b) + self.assertEqual(Rack.objects.get(pk=rack1.pk).site, site_b) + self.assertEqual(Rack.objects.get(pk=rack2.pk).site, site_b) + + class RackTestCase(TestCase): def setUp(self): @@ -154,6 +187,34 @@ class RackTestCase(TestCase): ) self.assertTrue(pdu) + def test_change_rack_site(self): + """ + Check that child Devices get updated when a Rack is moved to a new Site. + """ + site_a = Site.objects.create(name='Site A', slug='site-a') + site_b = Site.objects.create(name='Site B', slug='site-b') + + manufacturer = Manufacturer.objects.create(name='Manufacturer 1', slug='manufacturer-1') + device_type = DeviceType.objects.create( + manufacturer=manufacturer, model='Device Type 1', slug='device-type-1' + ) + device_role = DeviceRole.objects.create( + name='Device Role 1', slug='device-role-1', color='ff0000' + ) + + # Create Rack1 in Site A + rack1 = Rack.objects.create(site=site_a, name='Rack 1') + + # Create Device1 in Rack1 + device1 = Device.objects.create(site=site_a, rack=rack1, device_type=device_type, device_role=device_role) + + # Move Rack1 to Site B + rack1.site = site_b + rack1.save() + + # Check that Device1 is now assigned to Site B + self.assertEqual(Device.objects.get(pk=device1.pk).site, site_b) + class DeviceTestCase(TestCase): From ddd10ba8af5f619f485245d08bdcf1b2e82d74be Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Tue, 22 Dec 2020 16:14:16 -0500 Subject: [PATCH 06/28] Clean up hierarchical table columns --- netbox/dcim/tables/template_code.py | 11 ++++------- netbox/tenancy/tables.py | 11 ++++------- 2 files changed, 8 insertions(+), 14 deletions(-) diff --git a/netbox/dcim/tables/template_code.py b/netbox/dcim/tables/template_code.py index 4a5aedb29..ee1dc091b 100644 --- a/netbox/dcim/tables/template_code.py +++ b/netbox/dcim/tables/template_code.py @@ -57,13 +57,10 @@ INTERFACE_TAGGED_VLANS = """ """ MPTT_LINK = """ -{% if record.get_children %} - -{% else %} - -{% endif %} - {{ record.name }} - +{% for i in record.get_ancestors %} + +{% endfor %} +{{ record.name }} """ POWERFEED_CABLE = """ diff --git a/netbox/tenancy/tables.py b/netbox/tenancy/tables.py index 8abca2bb8..7a3d20e12 100644 --- a/netbox/tenancy/tables.py +++ b/netbox/tenancy/tables.py @@ -4,13 +4,10 @@ from utilities.tables import BaseTable, ButtonsColumn, LinkedCountColumn, TagCol from .models import Tenant, TenantGroup MPTT_LINK = """ -{% if record.get_children %} - -{% else %} - -{% endif %} - {{ record.name }} - +{% for i in record.get_ancestors %} + +{% endfor %} +{{ record.name }} """ COL_TENANT = """ From 8d9d4cec051c8b0e5433db538b2ae0a5debb92ea Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Wed, 23 Dec 2020 14:02:05 -0500 Subject: [PATCH 07/28] Extend handle_rackgroup_site_change() receiver to update power panels --- netbox/dcim/signals.py | 6 ++++-- netbox/dcim/tests/test_models.py | 3 +++ 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/netbox/dcim/signals.py b/netbox/dcim/signals.py index 380c0aec0..277e3f060 100644 --- a/netbox/dcim/signals.py +++ b/netbox/dcim/signals.py @@ -2,13 +2,12 @@ import logging from cacheops import invalidate_obj from django.contrib.contenttypes.models import ContentType -from django.db.models import Q from django.db.models.signals import post_save, post_delete, pre_delete from django.db import transaction from django.dispatch import receiver from .choices import CableStatusChoices -from .models import Cable, CablePath, Device, PathEndpoint, Rack, RackGroup, VirtualChassis +from .models import Cable, CablePath, Device, PathEndpoint, PowerPanel, Rack, RackGroup, VirtualChassis def create_cablepath(node): @@ -54,6 +53,9 @@ def handle_rackgroup_site_change(instance, created, **kwargs): for rack in Rack.objects.filter(group=instance).exclude(site=instance.site): rack.site = instance.site rack.save() + for powerpanel in PowerPanel.objects.filter(rack_group=instance).exclude(site=instance.site): + powerpanel.site = instance.site + powerpanel.save() @receiver(post_save, sender=Rack) diff --git a/netbox/dcim/tests/test_models.py b/netbox/dcim/tests/test_models.py index a372bb099..184681e90 100644 --- a/netbox/dcim/tests/test_models.py +++ b/netbox/dcim/tests/test_models.py @@ -29,6 +29,8 @@ class RackGroupTestCase(TestCase): rack1 = Rack.objects.create(site=site_a, group=rackgroup_a1, name='Rack 1') rack2 = Rack.objects.create(site=site_a, group=rackgroup_a2, name='Rack 2') + powerpanel1 = PowerPanel.objects.create(site=site_a, rack_group=rackgroup_a1, name='Power Panel 1') + # Move RackGroup A1 to Site B rackgroup_a1.site = site_b rackgroup_a1.save() @@ -38,6 +40,7 @@ class RackGroupTestCase(TestCase): self.assertEqual(RackGroup.objects.get(pk=rackgroup_a2.pk).site, site_b) self.assertEqual(Rack.objects.get(pk=rack1.pk).site, site_b) self.assertEqual(Rack.objects.get(pk=rack2.pk).site, site_b) + self.assertEqual(PowerPanel.objects.get(pk=powerpanel1.pk).site, site_b) class RackTestCase(TestCase): From 396b0dace8631cb7ac53ee3a841b3cc80a783873 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Wed, 23 Dec 2020 14:26:43 -0500 Subject: [PATCH 08/28] Changelog for #5311 --- docs/release-notes/version-2.10.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes/version-2.10.md b/docs/release-notes/version-2.10.md index 91a65fbfe..ba1d241d3 100644 --- a/docs/release-notes/version-2.10.md +++ b/docs/release-notes/version-2.10.md @@ -4,6 +4,7 @@ ### Bug Fixes +* [#5311](https://github.com/netbox-community/netbox/issues/5311) - Update child objects when a rack group is moved to a new site * [#5518](https://github.com/netbox-community/netbox/issues/5518) - Fix persistent vertical scrollbar --- From fce61295c9336fb8c82796d8a0d9571521b22146 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Wed, 23 Dec 2020 16:21:26 -0500 Subject: [PATCH 09/28] Fixes #5301: Fix misleading error when racking a device with invalid parameters --- docs/release-notes/version-2.10.md | 1 + netbox/dcim/forms.py | 39 +++++++++--------------------- netbox/dcim/models/devices.py | 10 ++++---- netbox/dcim/tests/test_forms.py | 18 ++++++++++++-- 4 files changed, 34 insertions(+), 34 deletions(-) diff --git a/docs/release-notes/version-2.10.md b/docs/release-notes/version-2.10.md index ba1d241d3..8cca1f1bf 100644 --- a/docs/release-notes/version-2.10.md +++ b/docs/release-notes/version-2.10.md @@ -4,6 +4,7 @@ ### Bug Fixes +* [#5301](https://github.com/netbox-community/netbox/issues/5301) - Fix misleading error when racking a device with invalid parameters * [#5311](https://github.com/netbox-community/netbox/issues/5311) - Update child objects when a rack group is moved to a new site * [#5518](https://github.com/netbox-community/netbox/issues/5518) - Fix persistent vertical scrollbar diff --git a/netbox/dcim/forms.py b/netbox/dcim/forms.py index 7ecd4efd8..4cc1a532b 100644 --- a/netbox/dcim/forms.py +++ b/netbox/dcim/forms.py @@ -1781,9 +1781,8 @@ class DeviceForm(BootstrapMixin, TenancyForm, CustomFieldModelForm): 'group_id': '$rack_group', } ) - position = forms.TypedChoiceField( + position = forms.IntegerField( required=False, - empty_value=None, help_text="The lowest-numbered unit occupied by the device", widget=APISelect( api_url='/api/dcim/racks/{{rack}}/elevation/', @@ -1856,6 +1855,7 @@ class DeviceForm(BootstrapMixin, TenancyForm, CustomFieldModelForm): "config context", } widgets = { + 'face': StaticSelect2(), 'status': StaticSelect2(), 'primary_ip4': StaticSelect2(), 'primary_ip6': StaticSelect2(), @@ -1902,6 +1902,13 @@ class DeviceForm(BootstrapMixin, TenancyForm, CustomFieldModelForm): Q(manufacturer__isnull=True) | Q(manufacturer=self.instance.device_type.manufacturer) ) + # Disable rack assignment if this is a child device installed in a parent device + if self.instance.device_type.is_child_device and hasattr(self.instance, 'parent_bay'): + self.fields['site'].disabled = True + self.fields['rack'].disabled = True + self.initial['site'] = self.instance.parent_bay.device.site_id + self.initial['rack'] = self.instance.parent_bay.device.rack_id + else: # An object that doesn't exist yet can't have any IPs assigned to it @@ -1911,31 +1918,9 @@ class DeviceForm(BootstrapMixin, TenancyForm, CustomFieldModelForm): self.fields['primary_ip6'].widget.attrs['readonly'] = True # Rack position - pk = self.instance.pk if self.instance.pk else None - try: - if self.is_bound and self.data.get('rack') and str(self.data.get('face')): - position_choices = Rack.objects.get(pk=self.data['rack']) \ - .get_rack_units(face=self.data.get('face'), exclude=pk) - elif self.initial.get('rack') and str(self.initial.get('face')): - position_choices = Rack.objects.get(pk=self.initial['rack']) \ - .get_rack_units(face=self.initial.get('face'), exclude=pk) - else: - position_choices = [] - except Rack.DoesNotExist: - position_choices = [] - self.fields['position'].choices = [('', '---------')] + [ - (p['id'], { - 'label': p['name'], - 'disabled': bool(p['device'] and p['id'] != self.initial.get('position')), - }) for p in position_choices - ] - - # Disable rack assignment if this is a child device installed in a parent device - if pk and self.instance.device_type.is_child_device and hasattr(self.instance, 'parent_bay'): - self.fields['site'].disabled = True - self.fields['rack'].disabled = True - self.initial['site'] = self.instance.parent_bay.device.site_id - self.initial['rack'] = self.instance.parent_bay.device.rack_id + position = self.data.get('position') or self.initial.get('position') + if position: + self.fields['position'].widget.choices = [(position, f'U{position}')] class BaseDeviceCSVForm(CustomFieldModelCSVForm): diff --git a/netbox/dcim/models/devices.py b/netbox/dcim/models/devices.py index 5b685057e..29818ab98 100644 --- a/netbox/dcim/models/devices.py +++ b/netbox/dcim/models/devices.py @@ -640,7 +640,7 @@ class Device(ChangeLoggedModel, ConfigContextModel, CustomFieldModel): # Validate site/rack combination if self.rack and self.site != self.rack.site: raise ValidationError({ - 'rack': "Rack {} does not belong to site {}.".format(self.rack, self.site), + 'rack': f"Rack {self.rack} does not belong to site {self.site}.", }) if self.rack is None: @@ -650,7 +650,7 @@ class Device(ChangeLoggedModel, ConfigContextModel, CustomFieldModel): }) if self.position: raise ValidationError({ - 'face': "Cannot select a rack position without assigning a rack.", + 'position': "Cannot select a rack position without assigning a rack.", }) # Validate position/face combination @@ -662,7 +662,7 @@ class Device(ChangeLoggedModel, ConfigContextModel, CustomFieldModel): # Prevent 0U devices from being assigned to a specific position if self.position and self.device_type.u_height == 0: raise ValidationError({ - 'position': "A U0 device type ({}) cannot be assigned to a rack position.".format(self.device_type) + 'position': f"A U0 device type ({self.device_type}) cannot be assigned to a rack position." }) if self.rack: @@ -688,8 +688,8 @@ class Device(ChangeLoggedModel, ConfigContextModel, CustomFieldModel): ) if self.position and self.position not in available_units: raise ValidationError({ - 'position': "U{} is already occupied or does not have sufficient space to accommodate a(n) " - "{} ({}U).".format(self.position, self.device_type, self.device_type.u_height) + 'position': f"U{self.position} is already occupied or does not have sufficient space to " + f"accommodate this device type: {self.device_type} ({self.device_type.u_height}U)" }) except DeviceType.DoesNotExist: diff --git a/netbox/dcim/tests/test_forms.py b/netbox/dcim/tests/test_forms.py index e8cb73fe4..6eeffbc96 100644 --- a/netbox/dcim/tests/test_forms.py +++ b/netbox/dcim/tests/test_forms.py @@ -82,7 +82,7 @@ class DeviceTestCase(TestCase): self.assertTrue(form.is_valid()) self.assertTrue(form.save()) - def test_non_racked_device_with_face_position(self): + def test_non_racked_device_with_face(self): form = DeviceForm(data={ 'name': 'New Device', 'device_role': DeviceRole.objects.first().pk, @@ -92,12 +92,26 @@ class DeviceTestCase(TestCase): 'site': Site.objects.first().pk, 'rack': None, 'face': DeviceFaceChoices.FACE_REAR, - 'position': 10, 'platform': None, 'status': DeviceStatusChoices.STATUS_ACTIVE, }) self.assertFalse(form.is_valid()) self.assertIn('face', form.errors) + + def test_non_racked_device_with_position(self): + form = DeviceForm(data={ + 'name': 'New Device', + 'device_role': DeviceRole.objects.first().pk, + 'tenant': None, + 'manufacturer': Manufacturer.objects.first().pk, + 'device_type': DeviceType.objects.first().pk, + 'site': Site.objects.first().pk, + 'rack': None, + 'position': 10, + 'platform': None, + 'status': DeviceStatusChoices.STATUS_ACTIVE, + }) + self.assertFalse(form.is_valid()) self.assertIn('position', form.errors) From 1fe585741167c09b405c5749cee43108f79cad9c Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Mon, 28 Dec 2020 10:45:23 -0500 Subject: [PATCH 10/28] Fixes #5543: Fix rendering of config contexts with cluster assignment for devices --- docs/release-notes/version-2.10.md | 1 + netbox/extras/querysets.py | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/release-notes/version-2.10.md b/docs/release-notes/version-2.10.md index 8cca1f1bf..8e2703d10 100644 --- a/docs/release-notes/version-2.10.md +++ b/docs/release-notes/version-2.10.md @@ -7,6 +7,7 @@ * [#5301](https://github.com/netbox-community/netbox/issues/5301) - Fix misleading error when racking a device with invalid parameters * [#5311](https://github.com/netbox-community/netbox/issues/5311) - Update child objects when a rack group is moved to a new site * [#5518](https://github.com/netbox-community/netbox/issues/5518) - Fix persistent vertical scrollbar +* [#5543](https://github.com/netbox-community/netbox/issues/5543) - Fix rendering of config contexts with cluster assignment for devices --- diff --git a/netbox/extras/querysets.py b/netbox/extras/querysets.py index 8276787db..fbe7fe903 100644 --- a/netbox/extras/querysets.py +++ b/netbox/extras/querysets.py @@ -89,6 +89,8 @@ class ConfigContextModelQuerySet(RestrictedQuerySet): } base_query = Q( Q(platforms=OuterRef('platform')) | Q(platforms=None), + Q(cluster_groups=OuterRef('cluster__group')) | Q(cluster_groups=None), + Q(clusters=OuterRef('cluster')) | Q(clusters=None), Q(tenant_groups=OuterRef('tenant__group')) | Q(tenant_groups=None), Q(tenants=OuterRef('tenant')) | Q(tenants=None), Q( @@ -111,8 +113,6 @@ class ConfigContextModelQuerySet(RestrictedQuerySet): elif self.model._meta.model_name == 'virtualmachine': base_query.add((Q(roles=OuterRef('role')) | Q(roles=None)), Q.AND) - base_query.add((Q(cluster_groups=OuterRef('cluster__group')) | Q(cluster_groups=None)), Q.AND) - base_query.add((Q(clusters=OuterRef('cluster')) | Q(clusters=None)), Q.AND) base_query.add((Q(sites=OuterRef('cluster__site')) | Q(sites=None)), Q.AND) region_field = 'cluster__site__region' From 6f39e6599dfe46b8448001f82a29724a7365dafc Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Mon, 28 Dec 2020 10:48:50 -0500 Subject: [PATCH 11/28] Fixes #5540: Fix exception when viewing a provider with one or more tags assigned --- docs/release-notes/version-2.10.md | 1 + netbox/templates/circuits/provider.html | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/release-notes/version-2.10.md b/docs/release-notes/version-2.10.md index 8e2703d10..0db155df5 100644 --- a/docs/release-notes/version-2.10.md +++ b/docs/release-notes/version-2.10.md @@ -7,6 +7,7 @@ * [#5301](https://github.com/netbox-community/netbox/issues/5301) - Fix misleading error when racking a device with invalid parameters * [#5311](https://github.com/netbox-community/netbox/issues/5311) - Update child objects when a rack group is moved to a new site * [#5518](https://github.com/netbox-community/netbox/issues/5518) - Fix persistent vertical scrollbar +* [#5540](https://github.com/netbox-community/netbox/issues/5540) - Fix exception when viewing a provider with one or more tags assigned * [#5543](https://github.com/netbox-community/netbox/issues/5543) - Fix rendering of config contexts with cluster assignment for devices --- diff --git a/netbox/templates/circuits/provider.html b/netbox/templates/circuits/provider.html index e96d5abcb..8778c3ac2 100644 --- a/netbox/templates/circuits/provider.html +++ b/netbox/templates/circuits/provider.html @@ -100,7 +100,7 @@
{% include 'inc/custom_fields_panel.html' %} - {% include 'extras/inc/tags_panel.html' with tags=object.tags.all url='circuits:object_list' %} + {% include 'extras/inc/tags_panel.html' with tags=object.tags.all url='circuits:provider_list' %}
Comments From cc1a43e5d9a3d30d45ccf49a428b7d3f093373ba Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Mon, 28 Dec 2020 11:10:27 -0500 Subject: [PATCH 12/28] Fixes #5533: Fix bulk editing of objects with required custom fields --- docs/release-notes/version-2.10.md | 1 + netbox/netbox/views/generic.py | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/release-notes/version-2.10.md b/docs/release-notes/version-2.10.md index 0db155df5..82e4a07e0 100644 --- a/docs/release-notes/version-2.10.md +++ b/docs/release-notes/version-2.10.md @@ -7,6 +7,7 @@ * [#5301](https://github.com/netbox-community/netbox/issues/5301) - Fix misleading error when racking a device with invalid parameters * [#5311](https://github.com/netbox-community/netbox/issues/5311) - Update child objects when a rack group is moved to a new site * [#5518](https://github.com/netbox-community/netbox/issues/5518) - Fix persistent vertical scrollbar +* [#5533](https://github.com/netbox-community/netbox/issues/5533) - Fix bulk editing of objects with required custom fields * [#5540](https://github.com/netbox-community/netbox/issues/5540) - Fix exception when viewing a provider with one or more tags assigned * [#5543](https://github.com/netbox-community/netbox/issues/5543) - Fix rendering of config contexts with cluster assignment for devices diff --git a/netbox/netbox/views/generic.py b/netbox/netbox/views/generic.py index eb7b2542f..bd21d469c 100644 --- a/netbox/netbox/views/generic.py +++ b/netbox/netbox/views/generic.py @@ -798,8 +798,8 @@ class BulkEditView(GetReturnURLMixin, ObjectPermissionRequiredMixin, View): # Update custom fields for name in custom_fields: if name in form.nullable_fields and name in nullified_fields: - obj.custom_field_data.pop(name, None) - else: + obj.custom_field_data[name] = None + elif form.cleaned_data.get(name) not in (None, ''): obj.custom_field_data[name] = form.cleaned_data[name] obj.full_clean() From b2e05aafc1a99d14fea2ab9c446a8dd7b4cd6375 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Mon, 28 Dec 2020 12:54:42 -0500 Subject: [PATCH 13/28] Closes #5531: Ensure consistent calls to parent clean() methods for models, forms --- netbox/dcim/forms.py | 5 +++++ netbox/dcim/models/device_component_templates.py | 2 ++ netbox/dcim/models/device_components.py | 6 ++++++ netbox/dcim/models/racks.py | 1 + netbox/extras/models/customfields.py | 4 ++++ netbox/extras/models/models.py | 6 +++++- netbox/users/admin.py | 2 ++ netbox/utilities/forms/forms.py | 2 ++ netbox/virtualization/models.py | 1 + 9 files changed, 28 insertions(+), 1 deletion(-) diff --git a/netbox/dcim/forms.py b/netbox/dcim/forms.py index 4cc1a532b..b75f9ba9a 100644 --- a/netbox/dcim/forms.py +++ b/netbox/dcim/forms.py @@ -134,6 +134,7 @@ class ComponentForm(BootstrapMixin, forms.Form): ) def clean(self): + super().clean() # Validate that the number of components being created from both the name_pattern and label_pattern are equal if self.cleaned_data['label_pattern']: @@ -1438,6 +1439,7 @@ class FrontPortTemplateCreateForm(ComponentTemplateCreateForm): self.fields['rear_port_set'].choices = choices def clean(self): + super().clean() # Validate that the number of ports being created equals the number of selected (rear port, position) tuples front_port_count = len(self.cleaned_data['name_pattern']) @@ -2929,6 +2931,7 @@ class InterfaceBulkEditForm( self.fields['lag'].widget.attrs['disabled'] = True def clean(self): + super().clean() # Untagged interfaces cannot be assigned tagged VLANs if self.cleaned_data['mode'] == InterfaceModeChoices.MODE_ACCESS and self.cleaned_data['tagged_vlans']: @@ -3077,6 +3080,7 @@ class FrontPortCreateForm(ComponentCreateForm): self.fields['rear_port_set'].choices = choices def clean(self): + super().clean() # Validate that the number of ports being created equals the number of selected (rear port, position) tuples front_port_count = len(self.cleaned_data['name_pattern']) @@ -3909,6 +3913,7 @@ class CableBulkEditForm(BootstrapMixin, AddRemoveTagsForm, BulkEditForm): ] def clean(self): + super().clean() # Validate length/unit length = self.cleaned_data.get('length') diff --git a/netbox/dcim/models/device_component_templates.py b/netbox/dcim/models/device_component_templates.py index 7a94b3e1b..fe819d310 100644 --- a/netbox/dcim/models/device_component_templates.py +++ b/netbox/dcim/models/device_component_templates.py @@ -193,6 +193,7 @@ class PowerOutletTemplate(ComponentTemplateModel): unique_together = ('device_type', 'name') def clean(self): + super().clean() # Validate power port assignment if self.power_port and self.power_port.device_type != self.device_type: @@ -278,6 +279,7 @@ class FrontPortTemplate(ComponentTemplateModel): ) def clean(self): + super().clean() # Validate rear port assignment if self.rear_port.device_type != self.device_type: diff --git a/netbox/dcim/models/device_components.py b/netbox/dcim/models/device_components.py index 5d5825f5f..21ca92da8 100644 --- a/netbox/dcim/models/device_components.py +++ b/netbox/dcim/models/device_components.py @@ -316,6 +316,7 @@ class PowerPort(CableTermination, PathEndpoint, ComponentModel): ) def clean(self): + super().clean() if self.maximum_draw is not None and self.allocated_draw is not None: if self.allocated_draw > self.maximum_draw: @@ -425,6 +426,7 @@ class PowerOutlet(CableTermination, PathEndpoint, ComponentModel): ) def clean(self): + super().clean() # Validate power port assignment if self.power_port and self.power_port.device != self.device: @@ -555,6 +557,7 @@ class Interface(CableTermination, PathEndpoint, ComponentModel, BaseInterface): ) def clean(self): + super().clean() # Virtual interfaces cannot be connected if self.type in NONCONNECTABLE_IFACE_TYPES and ( @@ -668,6 +671,7 @@ class FrontPort(CableTermination, ComponentModel): ) def clean(self): + super().clean() # Validate rear port assignment if self.rear_port.device != self.device: @@ -711,6 +715,7 @@ class RearPort(CableTermination, ComponentModel): return reverse('dcim:rearport', kwargs={'pk': self.pk}) def clean(self): + super().clean() # Check that positions count is greater than or equal to the number of associated FrontPorts frontport_count = self.frontports.count() @@ -768,6 +773,7 @@ class DeviceBay(ComponentModel): ) def clean(self): + super().clean() # Validate that the parent Device can have DeviceBays if not self.device.device_type.is_parent_device: diff --git a/netbox/dcim/models/racks.py b/netbox/dcim/models/racks.py index de9021f35..ccc775954 100644 --- a/netbox/dcim/models/racks.py +++ b/netbox/dcim/models/racks.py @@ -109,6 +109,7 @@ class RackGroup(MPTTModel, ChangeLoggedModel): ) def clean(self): + super().clean() # Parent RackGroup (if any) must belong to the same Site if self.parent and self.parent.site != self.site: diff --git a/netbox/extras/models/customfields.py b/netbox/extras/models/customfields.py index 6f4c5f9e1..693158bc5 100644 --- a/netbox/extras/models/customfields.py +++ b/netbox/extras/models/customfields.py @@ -47,6 +47,8 @@ class CustomFieldModel(models.Model): ]) def clean(self): + super().clean() + custom_fields = {cf.name: cf for cf in CustomField.objects.get_for_model(self)} # Validate all field values @@ -172,6 +174,8 @@ class CustomField(models.Model): obj.save() def clean(self): + super().clean() + # Validate the field's default value (if any) if self.default is not None: try: diff --git a/netbox/extras/models/models.py b/netbox/extras/models/models.py index 8934d9732..4917a7e44 100644 --- a/netbox/extras/models/models.py +++ b/netbox/extras/models/models.py @@ -117,11 +117,15 @@ class Webhook(models.Model): return self.name def clean(self): + super().clean() + + # At least one action type must be selected if not self.type_create and not self.type_delete and not self.type_update: raise ValidationError( "You must select at least one type: create, update, and/or delete." ) + # CA file path requires SSL verification enabled if not self.ssl_verification and self.ca_file_path: raise ValidationError({ 'ca_file_path': 'Do not specify a CA certificate file if SSL verification is disabled.' @@ -436,6 +440,7 @@ class ConfigContext(ChangeLoggedModel): return reverse('extras:configcontext', kwargs={'pk': self.pk}) def clean(self): + super().clean() # Verify that JSON data is provided as an object if type(self.data) is not dict: @@ -482,7 +487,6 @@ class ConfigContextModel(models.Model): return data def clean(self): - super().clean() # Verify that JSON data is provided as an object diff --git a/netbox/users/admin.py b/netbox/users/admin.py index c03e6f740..f2fe4a0b4 100644 --- a/netbox/users/admin.py +++ b/netbox/users/admin.py @@ -169,6 +169,8 @@ class ObjectPermissionForm(forms.ModelForm): self.instance.actions.remove(action) def clean(self): + super().clean() + object_types = self.cleaned_data.get('object_types') constraints = self.cleaned_data.get('constraints') diff --git a/netbox/utilities/forms/forms.py b/netbox/utilities/forms/forms.py index f259f6b6d..e674afdf7 100644 --- a/netbox/utilities/forms/forms.py +++ b/netbox/utilities/forms/forms.py @@ -82,6 +82,7 @@ class BulkRenameForm(forms.Form): ) def clean(self): + super().clean() # Validate regular expression in "find" field if self.cleaned_data['use_regex']: @@ -124,6 +125,7 @@ class ImportForm(BootstrapMixin, forms.Form): ) def clean(self): + super().clean() data = self.cleaned_data['data'] format = self.cleaned_data['format'] diff --git a/netbox/virtualization/models.py b/netbox/virtualization/models.py index cf7737340..edca7e1fe 100644 --- a/netbox/virtualization/models.py +++ b/netbox/virtualization/models.py @@ -444,6 +444,7 @@ class VMInterface(BaseInterface): ) def clean(self): + super().clean() # Validate untagged VLAN if self.untagged_vlan and self.untagged_vlan.site not in [self.virtual_machine.site, None]: From 8ae3331d04dab7ad8dee285ad0913ac9dfde49ad Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Tue, 29 Dec 2020 11:41:44 -0500 Subject: [PATCH 14/28] Closes #5549: Eliminate extraneous database queries when using brief API calls --- netbox/circuits/api/views.py | 1 + netbox/dcim/api/views.py | 11 ++++++++ netbox/extras/api/views.py | 9 +++---- netbox/netbox/api/views.py | 40 ++++++++++++++++++------------ netbox/virtualization/api/views.py | 1 + 5 files changed, 41 insertions(+), 21 deletions(-) diff --git a/netbox/circuits/api/views.py b/netbox/circuits/api/views.py index 6968da61e..736871a73 100644 --- a/netbox/circuits/api/views.py +++ b/netbox/circuits/api/views.py @@ -65,3 +65,4 @@ class CircuitTerminationViewSet(PathEndpointMixin, ModelViewSet): ) serializer_class = serializers.CircuitTerminationSerializer filterset_class = filters.CircuitTerminationFilterSet + brief_prefetch_fields = ['circuit'] diff --git a/netbox/dcim/api/views.py b/netbox/dcim/api/views.py index f9e8027b4..ae39f6ad0 100644 --- a/netbox/dcim/api/views.py +++ b/netbox/dcim/api/views.py @@ -258,6 +258,7 @@ class DeviceTypeViewSet(CustomFieldModelViewSet): ) serializer_class = serializers.DeviceTypeSerializer filterset_class = filters.DeviceTypeFilterSet + brief_prefetch_fields = ['manufacturer'] # @@ -493,6 +494,7 @@ class ConsolePortViewSet(PathEndpointMixin, ModelViewSet): queryset = ConsolePort.objects.prefetch_related('device', '_path__destination', 'cable', '_cable_peer', 'tags') serializer_class = serializers.ConsolePortSerializer filterset_class = filters.ConsolePortFilterSet + brief_prefetch_fields = ['device'] class ConsoleServerPortViewSet(PathEndpointMixin, ModelViewSet): @@ -501,18 +503,21 @@ class ConsoleServerPortViewSet(PathEndpointMixin, ModelViewSet): ) serializer_class = serializers.ConsoleServerPortSerializer filterset_class = filters.ConsoleServerPortFilterSet + brief_prefetch_fields = ['device'] class PowerPortViewSet(PathEndpointMixin, ModelViewSet): queryset = PowerPort.objects.prefetch_related('device', '_path__destination', 'cable', '_cable_peer', 'tags') serializer_class = serializers.PowerPortSerializer filterset_class = filters.PowerPortFilterSet + brief_prefetch_fields = ['device'] class PowerOutletViewSet(PathEndpointMixin, ModelViewSet): queryset = PowerOutlet.objects.prefetch_related('device', '_path__destination', 'cable', '_cable_peer', 'tags') serializer_class = serializers.PowerOutletSerializer filterset_class = filters.PowerOutletFilterSet + brief_prefetch_fields = ['device'] class InterfaceViewSet(PathEndpointMixin, ModelViewSet): @@ -521,30 +526,35 @@ class InterfaceViewSet(PathEndpointMixin, ModelViewSet): ) serializer_class = serializers.InterfaceSerializer filterset_class = filters.InterfaceFilterSet + brief_prefetch_fields = ['device'] class FrontPortViewSet(PassThroughPortMixin, ModelViewSet): queryset = FrontPort.objects.prefetch_related('device__device_type__manufacturer', 'rear_port', 'cable', 'tags') serializer_class = serializers.FrontPortSerializer filterset_class = filters.FrontPortFilterSet + brief_prefetch_fields = ['device'] class RearPortViewSet(PassThroughPortMixin, ModelViewSet): queryset = RearPort.objects.prefetch_related('device__device_type__manufacturer', 'cable', 'tags') serializer_class = serializers.RearPortSerializer filterset_class = filters.RearPortFilterSet + brief_prefetch_fields = ['device'] class DeviceBayViewSet(ModelViewSet): queryset = DeviceBay.objects.prefetch_related('installed_device').prefetch_related('tags') serializer_class = serializers.DeviceBaySerializer filterset_class = filters.DeviceBayFilterSet + brief_prefetch_fields = ['device'] class InventoryItemViewSet(ModelViewSet): queryset = InventoryItem.objects.prefetch_related('device', 'manufacturer').prefetch_related('tags') serializer_class = serializers.InventoryItemSerializer filterset_class = filters.InventoryItemFilterSet + brief_prefetch_fields = ['device'] # @@ -600,6 +610,7 @@ class VirtualChassisViewSet(ModelViewSet): ) serializer_class = serializers.VirtualChassisSerializer filterset_class = filters.VirtualChassisFilterSet + brief_prefetch_fields = ['master'] # diff --git a/netbox/extras/api/views.py b/netbox/extras/api/views.py index 8ab7b0eea..1067ac0d3 100644 --- a/netbox/extras/api/views.py +++ b/netbox/extras/api/views.py @@ -39,7 +39,6 @@ class ConfigContextQuerySetMixin: Provides a get_queryset() method which deals with adding the config context data annotation or not. """ - def get_queryset(self): """ Build the proper queryset based on the request context @@ -49,11 +48,11 @@ class ConfigContextQuerySetMixin: Else, return the queryset annotated with config context data """ - + queryset = super().get_queryset() request = self.get_serializer_context()['request'] - if request.query_params.get('brief') or 'config_context' in request.query_params.get('exclude', []): - return self.queryset - return self.queryset.annotate_config_context_data() + if self.brief or 'config_context' in request.query_params.get('exclude', []): + return queryset + return queryset.annotate_config_context_data() # diff --git a/netbox/netbox/api/views.py b/netbox/netbox/api/views.py index 881effd71..991a8892d 100644 --- a/netbox/netbox/api/views.py +++ b/netbox/netbox/api/views.py @@ -9,11 +9,11 @@ from django.core.exceptions import ObjectDoesNotExist, PermissionDenied from django.db import transaction from django.db.models import ProtectedError from django_rq.queues import get_connection -from rest_framework import mixins, status +from rest_framework import status from rest_framework.response import Response from rest_framework.reverse import reverse from rest_framework.views import APIView -from rest_framework.viewsets import GenericViewSet +from rest_framework.viewsets import ModelViewSet as ModelViewSet_ from rq.worker import Worker from netbox.api import BulkOperationSerializer @@ -120,17 +120,13 @@ class BulkDestroyModelMixin: # Viewsets # -class ModelViewSet(mixins.CreateModelMixin, - mixins.RetrieveModelMixin, - mixins.UpdateModelMixin, - mixins.DestroyModelMixin, - mixins.ListModelMixin, - BulkUpdateModelMixin, - BulkDestroyModelMixin, - GenericViewSet): +class ModelViewSet(BulkUpdateModelMixin, BulkDestroyModelMixin, ModelViewSet_): """ - Accept either a single object or a list of objects to create. + Extend DRF's ModelViewSet to support bulk update and delete functions. """ + brief = False + brief_prefetch_fields = [] + def get_serializer(self, *args, **kwargs): # If a list of objects has been provided, initialize the serializer with many=True @@ -142,22 +138,34 @@ class ModelViewSet(mixins.CreateModelMixin, def get_serializer_class(self): logger = logging.getLogger('netbox.api.views.ModelViewSet') - # If 'brief' has been passed as a query param, find and return the nested serializer for this model, if one - # exists - request = self.get_serializer_context()['request'] - if request.query_params.get('brief'): + # If using 'brief' mode, find and return the nested serializer for this model, if one exists + if self.brief: logger.debug("Request is for 'brief' format; initializing nested serializer") try: serializer = get_serializer_for_model(self.queryset.model, prefix='Nested') logger.debug(f"Using serializer {serializer}") return serializer except SerializerNotFound: - pass + logger.debug(f"Nested serializer for {self.queryset.model} not found!") # Fall back to the hard-coded serializer class logger.debug(f"Using serializer {self.serializer_class}") return self.serializer_class + def get_queryset(self): + # If using brief mode, clear all prefetches from the queryset and append only brief_prefetch_fields (if any) + if self.brief: + return super().get_queryset().prefetch_related(None).prefetch_related(*self.brief_prefetch_fields) + + return super().get_queryset() + + def initialize_request(self, request, *args, **kwargs): + # Check if brief=True has been passed + if request.method == 'GET' and request.GET.get('brief'): + self.brief = True + + return super().initialize_request(request, *args, **kwargs) + def initial(self, request, *args, **kwargs): super().initial(request, *args, **kwargs) diff --git a/netbox/virtualization/api/views.py b/netbox/virtualization/api/views.py index ce5cb9f2c..586ad5028 100644 --- a/netbox/virtualization/api/views.py +++ b/netbox/virtualization/api/views.py @@ -84,3 +84,4 @@ class VMInterfaceViewSet(ModelViewSet): ) serializer_class = serializers.VMInterfaceSerializer filterset_class = filters.VMInterfaceFilterSet + brief_prefetch_fields = ['virtual_machine'] From 249948e17426989464b401023988c16529175fd8 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Tue, 29 Dec 2020 11:55:31 -0500 Subject: [PATCH 15/28] Fixes #5546: Add custom field bulk edit support for cables, power panels, rack reservations, and virtual chassis --- docs/release-notes/version-2.10.md | 1 + netbox/dcim/forms.py | 12 +++++------- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/docs/release-notes/version-2.10.md b/docs/release-notes/version-2.10.md index 82e4a07e0..b5a81be9b 100644 --- a/docs/release-notes/version-2.10.md +++ b/docs/release-notes/version-2.10.md @@ -10,6 +10,7 @@ * [#5533](https://github.com/netbox-community/netbox/issues/5533) - Fix bulk editing of objects with required custom fields * [#5540](https://github.com/netbox-community/netbox/issues/5540) - Fix exception when viewing a provider with one or more tags assigned * [#5543](https://github.com/netbox-community/netbox/issues/5543) - Fix rendering of config contexts with cluster assignment for devices +* [#5546](https://github.com/netbox-community/netbox/issues/5546) - Add custom field bulk edit support for cables, power panels, rack reservations, and virtual chassis --- diff --git a/netbox/dcim/forms.py b/netbox/dcim/forms.py index b75f9ba9a..7ba6a4541 100644 --- a/netbox/dcim/forms.py +++ b/netbox/dcim/forms.py @@ -834,7 +834,7 @@ class RackReservationCSVForm(CSVModelForm): self.fields['rack'].queryset = self.fields['rack'].queryset.filter(**params) -class RackReservationBulkEditForm(BootstrapMixin, AddRemoveTagsForm, BulkEditForm): +class RackReservationBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditForm): pk = forms.ModelMultipleChoiceField( queryset=RackReservation.objects.all(), widget=forms.MultipleHiddenInput() @@ -3870,7 +3870,7 @@ class CableCSVForm(CSVModelForm): return length_unit if length_unit is not None else '' -class CableBulkEditForm(BootstrapMixin, AddRemoveTagsForm, BulkEditForm): +class CableBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditForm): pk = forms.ModelMultipleChoiceField( queryset=Cable.objects.all(), widget=forms.MultipleHiddenInput @@ -4257,7 +4257,7 @@ class VCMemberSelectForm(BootstrapMixin, forms.Form): return device -class VirtualChassisBulkEditForm(BootstrapMixin, AddRemoveTagsForm, BulkEditForm): +class VirtualChassisBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditForm): pk = forms.ModelMultipleChoiceField( queryset=VirtualChassis.objects.all(), widget=forms.MultipleHiddenInput() @@ -4384,7 +4384,7 @@ class PowerPanelCSVForm(CSVModelForm): self.fields['rack_group'].queryset = self.fields['rack_group'].queryset.filter(**params) -class PowerPanelBulkEditForm(BootstrapMixin, AddRemoveTagsForm, BulkEditForm): +class PowerPanelBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditForm): pk = forms.ModelMultipleChoiceField( queryset=PowerPanel.objects.all(), widget=forms.MultipleHiddenInput @@ -4412,9 +4412,7 @@ class PowerPanelBulkEditForm(BootstrapMixin, AddRemoveTagsForm, BulkEditForm): ) class Meta: - nullable_fields = ( - 'rack_group', - ) + nullable_fields = ['rack_group'] class PowerPanelFilterForm(BootstrapMixin, CustomFieldFilterForm): From d989ce2b706bed864dd4aeee20a86c2ac565a252 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Tue, 29 Dec 2020 12:43:52 -0500 Subject: [PATCH 16/28] Fixes #5547: Add custom field bulk import support for cables, power panels, rack reservations, and virtual chassis --- docs/release-notes/version-2.10.md | 1 + netbox/dcim/forms.py | 8 ++++---- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/docs/release-notes/version-2.10.md b/docs/release-notes/version-2.10.md index b5a81be9b..9bc10416a 100644 --- a/docs/release-notes/version-2.10.md +++ b/docs/release-notes/version-2.10.md @@ -11,6 +11,7 @@ * [#5540](https://github.com/netbox-community/netbox/issues/5540) - Fix exception when viewing a provider with one or more tags assigned * [#5543](https://github.com/netbox-community/netbox/issues/5543) - Fix rendering of config contexts with cluster assignment for devices * [#5546](https://github.com/netbox-community/netbox/issues/5546) - Add custom field bulk edit support for cables, power panels, rack reservations, and virtual chassis +* [#5547](https://github.com/netbox-community/netbox/issues/5547) - Add custom field bulk import support for cables, power panels, rack reservations, and virtual chassis --- diff --git a/netbox/dcim/forms.py b/netbox/dcim/forms.py index 7ba6a4541..f7eb510ec 100644 --- a/netbox/dcim/forms.py +++ b/netbox/dcim/forms.py @@ -784,7 +784,7 @@ class RackReservationForm(BootstrapMixin, TenancyForm, CustomFieldModelForm): ] -class RackReservationCSVForm(CSVModelForm): +class RackReservationCSVForm(CustomFieldModelCSVForm): site = CSVModelChoiceField( queryset=Site.objects.all(), to_field_name='name', @@ -3775,7 +3775,7 @@ class CableForm(BootstrapMixin, CustomFieldModelForm): } -class CableCSVForm(CSVModelForm): +class CableCSVForm(CustomFieldModelCSVForm): # Termination A side_a_device = CSVModelChoiceField( queryset=Device.objects.all(), @@ -4271,7 +4271,7 @@ class VirtualChassisBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldB nullable_fields = ['domain'] -class VirtualChassisCSVForm(CSVModelForm): +class VirtualChassisCSVForm(CustomFieldModelCSVForm): master = CSVModelChoiceField( queryset=Device.objects.all(), to_field_name='name', @@ -4358,7 +4358,7 @@ class PowerPanelForm(BootstrapMixin, CustomFieldModelForm): ] -class PowerPanelCSVForm(CSVModelForm): +class PowerPanelCSVForm(CustomFieldModelCSVForm): site = CSVModelChoiceField( queryset=Site.objects.all(), to_field_name='name', From 7873952e7a3b95e479274415c5325eeb33dbaa7f Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Wed, 30 Dec 2020 15:30:31 -0500 Subject: [PATCH 17/28] Avoid wrapping text in hierarchical table columns --- netbox/dcim/tables/racks.py | 3 ++- netbox/dcim/tables/sites.py | 3 ++- netbox/ipam/tables.py | 2 +- netbox/tenancy/tables.py | 3 ++- 4 files changed, 7 insertions(+), 4 deletions(-) diff --git a/netbox/dcim/tables/racks.py b/netbox/dcim/tables/racks.py index 85943dd0d..775e90076 100644 --- a/netbox/dcim/tables/racks.py +++ b/netbox/dcim/tables/racks.py @@ -26,7 +26,8 @@ class RackGroupTable(BaseTable): pk = ToggleColumn() name = tables.TemplateColumn( template_code=MPTT_LINK, - orderable=False + orderable=False, + attrs={'td': {'class': 'text-nowrap'}} ) site = tables.LinkColumn( viewname='dcim:site', diff --git a/netbox/dcim/tables/sites.py b/netbox/dcim/tables/sites.py index 76f30f507..50a5e5ec7 100644 --- a/netbox/dcim/tables/sites.py +++ b/netbox/dcim/tables/sites.py @@ -19,7 +19,8 @@ class RegionTable(BaseTable): pk = ToggleColumn() name = tables.TemplateColumn( template_code=MPTT_LINK, - orderable=False + orderable=False, + attrs={'td': {'class': 'text-nowrap'}} ) site_count = tables.Column( verbose_name='Sites' diff --git a/netbox/ipam/tables.py b/netbox/ipam/tables.py index bea8ec255..86d7e21eb 100644 --- a/netbox/ipam/tables.py +++ b/netbox/ipam/tables.py @@ -270,7 +270,7 @@ class PrefixTable(BaseTable): pk = ToggleColumn() prefix = tables.TemplateColumn( template_code=PREFIX_LINK, - attrs={'th': {'style': 'padding-left: 17px'}} + attrs={'td': {'class': 'text-nowrap'}} ) status = ChoiceFieldColumn( default=AVAILABLE_LABEL diff --git a/netbox/tenancy/tables.py b/netbox/tenancy/tables.py index 7a3d20e12..9e8be6b18 100644 --- a/netbox/tenancy/tables.py +++ b/netbox/tenancy/tables.py @@ -27,7 +27,8 @@ class TenantGroupTable(BaseTable): pk = ToggleColumn() name = tables.TemplateColumn( template_code=MPTT_LINK, - orderable=False + orderable=False, + attrs={'td': {'class': 'text-nowrap'}} ) tenant_count = LinkedCountColumn( viewname='tenancy:tenant_list', From af3c4905ea1a4ed24ad23b342f489013a18c252d Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Thu, 31 Dec 2020 10:21:01 -0500 Subject: [PATCH 18/28] Fixes #5558: Fix regex validation support for custom URL fields --- docs/release-notes/version-2.10.md | 1 + netbox/extras/models/customfields.py | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/release-notes/version-2.10.md b/docs/release-notes/version-2.10.md index 9bc10416a..69e31606f 100644 --- a/docs/release-notes/version-2.10.md +++ b/docs/release-notes/version-2.10.md @@ -12,6 +12,7 @@ * [#5543](https://github.com/netbox-community/netbox/issues/5543) - Fix rendering of config contexts with cluster assignment for devices * [#5546](https://github.com/netbox-community/netbox/issues/5546) - Add custom field bulk edit support for cables, power panels, rack reservations, and virtual chassis * [#5547](https://github.com/netbox-community/netbox/issues/5547) - Add custom field bulk import support for cables, power panels, rack reservations, and virtual chassis +* [#5558](https://github.com/netbox-community/netbox/issues/5558) - Fix regex validation support for custom URL fields --- diff --git a/netbox/extras/models/customfields.py b/netbox/extras/models/customfields.py index 693158bc5..a69816d21 100644 --- a/netbox/extras/models/customfields.py +++ b/netbox/extras/models/customfields.py @@ -196,7 +196,8 @@ class CustomField(models.Model): }) # Regex validation can be set only for text fields - if self.validation_regex and self.type != CustomFieldTypeChoices.TYPE_TEXT: + regex_types = (CustomFieldTypeChoices.TYPE_TEXT, CustomFieldTypeChoices.TYPE_URL) + if self.validation_regex and self.type not in regex_types: raise ValidationError({ 'validation_regex': "Regular expression validation is supported only for text and URL fields" }) From 39e6872288fdf7296dbe451150f25c536ae76b3d Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Thu, 31 Dec 2020 10:32:40 -0500 Subject: [PATCH 19/28] Fixes #5557: Fix VRF route target assignment via REST API --- docs/release-notes/version-2.10.md | 1 + netbox/ipam/api/serializers.py | 14 ++++++++++++-- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/docs/release-notes/version-2.10.md b/docs/release-notes/version-2.10.md index 69e31606f..241004f22 100644 --- a/docs/release-notes/version-2.10.md +++ b/docs/release-notes/version-2.10.md @@ -12,6 +12,7 @@ * [#5543](https://github.com/netbox-community/netbox/issues/5543) - Fix rendering of config contexts with cluster assignment for devices * [#5546](https://github.com/netbox-community/netbox/issues/5546) - Add custom field bulk edit support for cables, power panels, rack reservations, and virtual chassis * [#5547](https://github.com/netbox-community/netbox/issues/5547) - Add custom field bulk import support for cables, power panels, rack reservations, and virtual chassis +* [#5557](https://github.com/netbox-community/netbox/issues/5557) - Fix VRF route target assignment via REST API * [#5558](https://github.com/netbox-community/netbox/issues/5558) - Fix regex validation support for custom URL fields --- diff --git a/netbox/ipam/api/serializers.py b/netbox/ipam/api/serializers.py index 7552ae0d2..9b8d36590 100644 --- a/netbox/ipam/api/serializers.py +++ b/netbox/ipam/api/serializers.py @@ -25,8 +25,18 @@ from .nested_serializers import * class VRFSerializer(TaggedObjectSerializer, CustomFieldModelSerializer): url = serializers.HyperlinkedIdentityField(view_name='ipam-api:vrf-detail') tenant = NestedTenantSerializer(required=False, allow_null=True) - import_targets = NestedRouteTargetSerializer(required=False, allow_null=True, many=True) - export_targets = NestedRouteTargetSerializer(required=False, allow_null=True, many=True) + import_targets = SerializedPKRelatedField( + queryset=RouteTarget.objects.all(), + serializer=NestedRouteTargetSerializer, + required=False, + many=True + ) + export_targets = SerializedPKRelatedField( + queryset=RouteTarget.objects.all(), + serializer=NestedRouteTargetSerializer, + required=False, + many=True + ) ipaddress_count = serializers.IntegerField(read_only=True) prefix_count = serializers.IntegerField(read_only=True) From e73c22596546e0232135ae20b7132c02529ffff7 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Thu, 31 Dec 2020 10:38:47 -0500 Subject: [PATCH 20/28] Fixes #5551: Restore missing import button on services list --- docs/release-notes/version-2.10.md | 1 + netbox/ipam/views.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/release-notes/version-2.10.md b/docs/release-notes/version-2.10.md index 241004f22..b3b71657c 100644 --- a/docs/release-notes/version-2.10.md +++ b/docs/release-notes/version-2.10.md @@ -12,6 +12,7 @@ * [#5543](https://github.com/netbox-community/netbox/issues/5543) - Fix rendering of config contexts with cluster assignment for devices * [#5546](https://github.com/netbox-community/netbox/issues/5546) - Add custom field bulk edit support for cables, power panels, rack reservations, and virtual chassis * [#5547](https://github.com/netbox-community/netbox/issues/5547) - Add custom field bulk import support for cables, power panels, rack reservations, and virtual chassis +* [#5551](https://github.com/netbox-community/netbox/issues/5551) - Restore missing import button on services list * [#5557](https://github.com/netbox-community/netbox/issues/5557) - Fix VRF route target assignment via REST API * [#5558](https://github.com/netbox-community/netbox/issues/5558) - Fix regex validation support for custom URL fields diff --git a/netbox/ipam/views.py b/netbox/ipam/views.py index 36c225045..39a840f7f 100644 --- a/netbox/ipam/views.py +++ b/netbox/ipam/views.py @@ -804,7 +804,7 @@ class ServiceListView(generic.ObjectListView): filterset = filters.ServiceFilterSet filterset_form = forms.ServiceFilterForm table = tables.ServiceTable - action_buttons = ('export',) + action_buttons = ('import', 'export') class ServiceView(generic.ObjectView): From a9a2509d397e780669c7908f62867eb218f3070f Mon Sep 17 00:00:00 2001 From: Daniel Sheppard Date: Tue, 5 Jan 2021 08:48:55 -0600 Subject: [PATCH 21/28] Fixes: #5049 - Account for chassis neighbors in lldp_neighbors template --- docs/release-notes/version-2.10.md | 1 + netbox/templates/dcim/device/lldp_neighbors.html | 7 ++++--- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/docs/release-notes/version-2.10.md b/docs/release-notes/version-2.10.md index b3b71657c..85279eb24 100644 --- a/docs/release-notes/version-2.10.md +++ b/docs/release-notes/version-2.10.md @@ -4,6 +4,7 @@ ### Bug Fixes +* [#5049](https://github.com/netbox-community/netbox/issues/5049) - Add check for LLDP neighbor chassis name to lldp_neighbors * [#5301](https://github.com/netbox-community/netbox/issues/5301) - Fix misleading error when racking a device with invalid parameters * [#5311](https://github.com/netbox-community/netbox/issues/5311) - Update child objects when a rack group is moved to a new site * [#5518](https://github.com/netbox-community/netbox/issues/5518) - Fix persistent vertical scrollbar diff --git a/netbox/templates/dcim/device/lldp_neighbors.html b/netbox/templates/dcim/device/lldp_neighbors.html index 3216b3791..a2baa6322 100644 --- a/netbox/templates/dcim/device/lldp_neighbors.html +++ b/netbox/templates/dcim/device/lldp_neighbors.html @@ -23,7 +23,7 @@ {{ iface }} {% if iface.connected_endpoint.device %} - + {{ iface.connected_endpoint.device }} @@ -61,6 +61,7 @@ $(document).ready(function() { // Glean configured hostnames/interfaces from the DOM var configured_device = row.children('td.configured_device').attr('data'); + var configured_chassis = row.children('td.configured_device').attr('data-chassis'); var configured_interface = row.children('td.configured_interface').attr('data'); var configured_interface_short = null; if (configured_interface) { @@ -81,9 +82,9 @@ $(document).ready(function() { // Apply colors to rows if (!configured_device && lldp_device) { row.addClass('info'); - } else if (configured_device == lldp_device && configured_interface == lldp_interface) { + } else if ((configured_device == lldp_device || configured_chassis == lldp_device) && configured_interface == lldp_interface) { row.addClass('success'); - } else if (configured_device == lldp_device && configured_interface_short == lldp_interface) { + } else if ((configured_device == lldp_device || configured_chassis == lldp_device) && configured_interface_short == lldp_interface) { row.addClass('success'); } else { row.addClass('danger'); From 359ae5d116f983039b87b4b694b822116a32e652 Mon Sep 17 00:00:00 2001 From: Brian Candler Date: Tue, 5 Jan 2021 15:05:17 +0000 Subject: [PATCH 22/28] Raise exceptions for other inconsistencies when migrating custom field data Fixes #5573 --- netbox/extras/migrations/0051_migrate_customfields.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/netbox/extras/migrations/0051_migrate_customfields.py b/netbox/extras/migrations/0051_migrate_customfields.py index 2c992c700..41b2febe7 100644 --- a/netbox/extras/migrations/0051_migrate_customfields.py +++ b/netbox/extras/migrations/0051_migrate_customfields.py @@ -67,7 +67,7 @@ def migrate_customfieldvalues(apps, schema_editor): cf_data = model.objects.filter(pk=cfv.obj_id).values('custom_field_data').first() try: cf_data['custom_field_data'][cfv.field.name] = deserialize_value(cfv.field, cfv.serialized_value) - except ValueError as e: + except Exception as e: print(f'{cfv.field.name} ({cfv.field.type}): {cfv.serialized_value} ({cfv.pk})') raise e model.objects.filter(pk=cfv.obj_id).update(**cf_data) From d16a7e108ca3040fde3d97b97f992242efbd7b61 Mon Sep 17 00:00:00 2001 From: Daniel Sheppard Date: Tue, 5 Jan 2021 09:22:10 -0600 Subject: [PATCH 23/28] Fixes: #5563 - Fix power feed cable trace --- docs/release-notes/version-2.10.md | 1 + netbox/templates/dcim/powerfeed.html | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/release-notes/version-2.10.md b/docs/release-notes/version-2.10.md index 85279eb24..b9a3685cc 100644 --- a/docs/release-notes/version-2.10.md +++ b/docs/release-notes/version-2.10.md @@ -16,6 +16,7 @@ * [#5551](https://github.com/netbox-community/netbox/issues/5551) - Restore missing import button on services list * [#5557](https://github.com/netbox-community/netbox/issues/5557) - Fix VRF route target assignment via REST API * [#5558](https://github.com/netbox-community/netbox/issues/5558) - Fix regex validation support for custom URL fields +* [#5563](https://github.com/netbox-community/netbox/issues/5563) - Fix power feed cable trace link --- diff --git a/netbox/templates/dcim/powerfeed.html b/netbox/templates/dcim/powerfeed.html index eded66fec..be7e03ec5 100644 --- a/netbox/templates/dcim/powerfeed.html +++ b/netbox/templates/dcim/powerfeed.html @@ -165,7 +165,7 @@ Cable {{ object.cable }} - + From 3441216aca619c18872f55fbb4e8369739701356 Mon Sep 17 00:00:00 2001 From: Daniel Sheppard Date: Tue, 5 Jan 2021 09:52:11 -0600 Subject: [PATCH 24/28] Fixes: #5564 - Raise validation error if a PowerPortTemplate's draw exceeds maximum --- docs/release-notes/version-2.10.md | 1 + netbox/dcim/models/device_component_templates.py | 9 +++++++++ 2 files changed, 10 insertions(+) diff --git a/docs/release-notes/version-2.10.md b/docs/release-notes/version-2.10.md index b9a3685cc..4b762f1ae 100644 --- a/docs/release-notes/version-2.10.md +++ b/docs/release-notes/version-2.10.md @@ -17,6 +17,7 @@ * [#5557](https://github.com/netbox-community/netbox/issues/5557) - Fix VRF route target assignment via REST API * [#5558](https://github.com/netbox-community/netbox/issues/5558) - Fix regex validation support for custom URL fields * [#5563](https://github.com/netbox-community/netbox/issues/5563) - Fix power feed cable trace link +* [#5564](https://github.com/netbox-community/netbox/issues/5564) - Raise validation error if a power port template's `allocated_draw` exceeds its `maximum_draw` --- diff --git a/netbox/dcim/models/device_component_templates.py b/netbox/dcim/models/device_component_templates.py index fe819d310..58233f3bf 100644 --- a/netbox/dcim/models/device_component_templates.py +++ b/netbox/dcim/models/device_component_templates.py @@ -164,6 +164,15 @@ class PowerPortTemplate(ComponentTemplateModel): allocated_draw=self.allocated_draw ) + def clean(self): + super().clean() + + if self.maximum_draw is not None and self.allocated_draw is not None: + if self.allocated_draw > self.maximum_draw: + raise ValidationError({ + 'allocated_draw': f"Allocated draw cannot exceed the maximum draw ({self.maximum_draw}W)." + }) + class PowerOutletTemplate(ComponentTemplateModel): """ From aa10430c7b2f484c9fc74e994a2a15239f06ce33 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Tue, 5 Jan 2021 15:06:15 -0500 Subject: [PATCH 25/28] Changelog for #5573 --- docs/release-notes/version-2.10.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes/version-2.10.md b/docs/release-notes/version-2.10.md index 4b762f1ae..d46782f8f 100644 --- a/docs/release-notes/version-2.10.md +++ b/docs/release-notes/version-2.10.md @@ -18,6 +18,7 @@ * [#5558](https://github.com/netbox-community/netbox/issues/5558) - Fix regex validation support for custom URL fields * [#5563](https://github.com/netbox-community/netbox/issues/5563) - Fix power feed cable trace link * [#5564](https://github.com/netbox-community/netbox/issues/5564) - Raise validation error if a power port template's `allocated_draw` exceeds its `maximum_draw` +* [#5573](https://github.com/netbox-community/netbox/issues/5573) - Report inconsistent values when migrating custom field data --- From 0accaedad01def9cf0a9f3907ed4c61d9030798a Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Tue, 5 Jan 2021 15:15:36 -0500 Subject: [PATCH 26/28] Fixes #5569: Ensure consistent labeling of interface mgmt_only field --- docs/release-notes/version-2.10.md | 1 + netbox/dcim/models/device_components.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/release-notes/version-2.10.md b/docs/release-notes/version-2.10.md index d46782f8f..03ac644da 100644 --- a/docs/release-notes/version-2.10.md +++ b/docs/release-notes/version-2.10.md @@ -18,6 +18,7 @@ * [#5558](https://github.com/netbox-community/netbox/issues/5558) - Fix regex validation support for custom URL fields * [#5563](https://github.com/netbox-community/netbox/issues/5563) - Fix power feed cable trace link * [#5564](https://github.com/netbox-community/netbox/issues/5564) - Raise validation error if a power port template's `allocated_draw` exceeds its `maximum_draw` +* [#5569](https://github.com/netbox-community/netbox/issues/5569) - Ensure consistent labeling of interface `mgmt_only` field * [#5573](https://github.com/netbox-community/netbox/issues/5573) - Report inconsistent values when migrating custom field data --- diff --git a/netbox/dcim/models/device_components.py b/netbox/dcim/models/device_components.py index 21ca92da8..452aacb56 100644 --- a/netbox/dcim/models/device_components.py +++ b/netbox/dcim/models/device_components.py @@ -505,7 +505,7 @@ class Interface(CableTermination, PathEndpoint, ComponentModel, BaseInterface): ) mgmt_only = models.BooleanField( default=False, - verbose_name='OOB Management', + verbose_name='Management only', help_text='This interface is used only for out-of-band management' ) untagged_vlan = models.ForeignKey( From 4a2d2882c66782d994564556501015e23de8ef9c Mon Sep 17 00:00:00 2001 From: Brandon Bennett Date: Tue, 5 Jan 2021 15:51:03 -0700 Subject: [PATCH 27/28] Fixes #5579: mark `ie` and `nie` filter exprs as insensitive In the documentation the `ie` and `nie` filter expressions are incorrectly marked as senstive matches when they are in fact insensitive. --- docs/rest-api/filtering.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/rest-api/filtering.md b/docs/rest-api/filtering.md index 1ec6752a4..b77513297 100644 --- a/docs/rest-api/filtering.md +++ b/docs/rest-api/filtering.md @@ -78,8 +78,8 @@ String based (char) fields (Name, Address, etc) support these lookup expressions - `nisw` - negated case insensitive starts with - `iew` - case insensitive ends with - `niew` - negated case insensitive ends with -- `ie` - case sensitive exact match -- `nie` - negated case sensitive exact match +- `ie` - case insensitive exact match +- `nie` - negated case insensitive exact match ### Foreign Keys & Other Fields From 601cbd2306a61c65a392b8e07bb1da2b4e9c86b5 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Tue, 5 Jan 2021 20:33:46 -0500 Subject: [PATCH 28/28] Release v2.10.3 --- docs/release-notes/version-2.10.md | 2 +- netbox/netbox/settings.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/release-notes/version-2.10.md b/docs/release-notes/version-2.10.md index 03ac644da..a3ab5968c 100644 --- a/docs/release-notes/version-2.10.md +++ b/docs/release-notes/version-2.10.md @@ -1,6 +1,6 @@ # NetBox v2.10 -## v2.10.3 (FUTURE) +## v2.10.3 (2021-01-05) ### Bug Fixes diff --git a/netbox/netbox/settings.py b/netbox/netbox/settings.py index 987452255..4c36332fc 100644 --- a/netbox/netbox/settings.py +++ b/netbox/netbox/settings.py @@ -16,7 +16,7 @@ from django.core.validators import URLValidator # Environment setup # -VERSION = '2.10.3-dev' +VERSION = '2.10.3' # Hostname HOSTNAME = platform.node()