From 7216dad2b4a36afbb1c7f8a086aadbf401350d52 Mon Sep 17 00:00:00 2001 From: Louis Date: Wed, 19 Aug 2026 10:43:33 +0200 Subject: [PATCH] fix: separate rack width import locks --- README.md | 6 ++++++ netbox_utilities/__init__.py | 2 +- netbox_utilities/netbox_export.py | 19 ++++++++++++++++--- netbox_utilities/tests/test_netbox_export.py | 16 ++++++++++++++-- netbox_utilities/tests/test_reorder_rack.py | 2 +- netbox_utilities/tests/test_topology_views.py | 2 +- pyproject.toml | 2 +- 7 files changed, 40 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 63d9d5c..4576125 100644 --- a/README.md +++ b/README.md @@ -281,6 +281,12 @@ entfernt der Import deshalb seine veraltete Teilbreitenzuordnung. Ältere Archive ohne diese Zusatzmetadaten bleiben importierbar; vorhandene `DeviceRackPlacement`-Datensätze darin werden weiterhin berücksichtigt. +Ab Version `0.9.10` sperrt der Import Geräte und ihre optionalen +Rackbreitenzuordnungen in getrennten Datenbankabfragen. Damit funktioniert der +Test- und Echtimport auch unter PostgreSQL, ohne einen unzulässigen +`FOR UPDATE`-Outer-Join zu erzeugen. Bereits mit Version `0.9.9` erstellte +Archive müssen dafür nicht neu exportiert werden. + ### Teilbreiten in MrBlake NetBox Topology Views Ist diff --git a/netbox_utilities/__init__.py b/netbox_utilities/__init__.py index 5210f77..8bca608 100644 --- a/netbox_utilities/__init__.py +++ b/netbox_utilities/__init__.py @@ -1,6 +1,6 @@ from netbox.plugins import PluginConfig, get_plugin_config -__version__ = "0.9.9" +__version__ = "0.9.10" class NetBoxUtilitiesConfig(PluginConfig): diff --git a/netbox_utilities/netbox_export.py b/netbox_utilities/netbox_export.py index 679680a..cd2d43b 100644 --- a/netbox_utilities/netbox_export.py +++ b/netbox_utilities/netbox_export.py @@ -91,19 +91,32 @@ def _width_aware_device_placement_conflicts(original, device, rack, position, fa width, horizontal_position = get_width_position(device) height, full_depth = _device_footprint(device) - candidates = ( + candidates = list( type(device) ._default_manager.select_for_update() - .select_related("device_type", "netbox_utilities_rack_placement") + .select_related("device_type") .filter(rack=rack, position__isnull=False) .exclude(pk=device.pk) ) + stored_placements = { + placement.device_id: placement + for placement in DeviceRackPlacement.objects.select_for_update().filter( + device_id__in=[candidate.pk for candidate in candidates] + ) + } conflicts = [] for candidate in candidates: candidate_height, candidate_full_depth = _device_footprint(candidate) if not (full_depth or candidate_full_depth or candidate.face == face): continue - candidate_width, candidate_horizontal_position = get_width_position(candidate) + stored_placement = stored_placements.get(candidate.pk) + if stored_placement is None: + candidate_width, candidate_horizontal_position = FULL_WIDTH, 1 + else: + candidate_width, candidate_horizontal_position = normalize_width_position( + stored_placement.width, + stored_placement.horizontal_position, + ) if placement_rectangles_overlap( position, height, diff --git a/netbox_utilities/tests/test_netbox_export.py b/netbox_utilities/tests/test_netbox_export.py index c634dd0..0b2295d 100644 --- a/netbox_utilities/tests/test_netbox_export.py +++ b/netbox_utilities/tests/test_netbox_export.py @@ -18,10 +18,15 @@ from netbox_utilities.netbox_export import ( class FakeQuerySet(list): + def __init__(self, values=()): + super().__init__(values) + self.selected_related = [] + def select_for_update(self): return self def select_related(self, *args): + self.selected_related.append(args) return self def filter(self, **kwargs): @@ -29,7 +34,9 @@ class FakeQuerySet(list): def exclude(self, **kwargs): pk = kwargs.get("pk") - return FakeQuerySet(item for item in self if item.pk != pk) + result = FakeQuerySet(item for item in self if item.pk != pk) + result.selected_related = self.selected_related + return result class FakeDevice: @@ -111,17 +118,22 @@ class NetBoxExportRackWidthTest(SimpleTestCase): self.assertIs(_defer_rack_width(original, FakeDevice, record)[1], native) - def test_import_conflicts_respect_horizontal_rack_positions(self): + @patch("netbox_utilities.netbox_export.DeviceRackPlacement.objects") + def test_import_conflicts_respect_horizontal_rack_positions(self, placement_objects): left = FakeDevice(1, width=2, horizontal_position=1, position=11) right = FakeDevice(2, width=2, horizontal_position=2) right.rack_id = 1 FakeDevice._default_manager = FakeQuerySet([left]) + stored_left = SimpleNamespace(device_id=1, width=2, horizontal_position=1) + placement_objects.select_for_update.return_value.filter.return_value = [stored_left] original = MagicMock() conflicts = _width_aware_device_placement_conflicts(original, right, object(), 11, "front") self.assertEqual(conflicts, []) original.assert_not_called() + self.assertEqual(FakeDevice._default_manager.selected_related, [("device_type",)]) + placement_objects.select_for_update.return_value.filter.assert_called_with(device_id__in=[1]) overlapping = FakeDevice(3, width=2, horizontal_position=1) overlapping.rack_id = 1 diff --git a/netbox_utilities/tests/test_reorder_rack.py b/netbox_utilities/tests/test_reorder_rack.py index 2ae57d6..0b76639 100644 --- a/netbox_utilities/tests/test_reorder_rack.py +++ b/netbox_utilities/tests/test_reorder_rack.py @@ -449,7 +449,7 @@ class ReorderRackFrontendTest(SimpleTestCase): self.assertEqual(template_name, "netbox_utilities/reorder_rack.html") self.assertEqual(context["reorder_devices"][0]["label"], "LEO-Fritzbox") self.assertEqual(context["reorder_devices"][0]["grid_width"], 6) - self.assertEqual(context["asset_version"], "0.9.9") + self.assertEqual(context["asset_version"], "0.9.10") self.assertIs(context["reorder_rack_width_data"], get_width_data.return_value) get_width_data.assert_called_once() self.assertIs(get_width_data.call_args.kwargs["rack"], rack) diff --git a/netbox_utilities/tests/test_topology_views.py b/netbox_utilities/tests/test_topology_views.py index 0acc5b2..a0d6aed 100644 --- a/netbox_utilities/tests/test_topology_views.py +++ b/netbox_utilities/tests/test_topology_views.py @@ -246,7 +246,7 @@ class TopologyViewsRackWidthTest(SimpleTestCase): self.assertIn('id="netbox-utilities-topology-rack-width-styles"', html) self.assertIn('.rack-device[href="/dcim/devices/334/"]', html) self.assertIn("left: calc(50% + 3px) !important", html) - self.assertIn("netbox_utilities/topology-rack-width.js?v=0.9.9", html) + self.assertIn("netbox_utilities/topology-rack-width.js?v=0.9.10", html) self.assertIn("left:calc(0% + 3px)!important", html) self.assertIn("left:calc(50% + 3px)!important", html) self.assertIn("width:calc(50% - 6px)!important", html) diff --git a/pyproject.toml b/pyproject.toml index 3f9a111..46c8a19 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "hatchling.build" [project] name = "netbox-utilities" -version = "0.9.9" +version = "0.9.10" description = "Navigation, tenant utilities, partial-width rack devices, bulk uploads, and rack reordering for NetBox 4.6" readme = "README.md" requires-python = ">=3.12"