fix: separate rack width import locks
This commit is contained in:
@@ -281,6 +281,12 @@ entfernt der Import deshalb seine veraltete Teilbreitenzuordnung. Ältere
|
|||||||
Archive ohne diese Zusatzmetadaten bleiben importierbar; vorhandene
|
Archive ohne diese Zusatzmetadaten bleiben importierbar; vorhandene
|
||||||
`DeviceRackPlacement`-Datensätze darin werden weiterhin berücksichtigt.
|
`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
|
### Teilbreiten in MrBlake NetBox Topology Views
|
||||||
|
|
||||||
Ist
|
Ist
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
from netbox.plugins import PluginConfig, get_plugin_config
|
from netbox.plugins import PluginConfig, get_plugin_config
|
||||||
|
|
||||||
__version__ = "0.9.9"
|
__version__ = "0.9.10"
|
||||||
|
|
||||||
|
|
||||||
class NetBoxUtilitiesConfig(PluginConfig):
|
class NetBoxUtilitiesConfig(PluginConfig):
|
||||||
|
|||||||
@@ -91,19 +91,32 @@ def _width_aware_device_placement_conflicts(original, device, rack, position, fa
|
|||||||
|
|
||||||
width, horizontal_position = get_width_position(device)
|
width, horizontal_position = get_width_position(device)
|
||||||
height, full_depth = _device_footprint(device)
|
height, full_depth = _device_footprint(device)
|
||||||
candidates = (
|
candidates = list(
|
||||||
type(device)
|
type(device)
|
||||||
._default_manager.select_for_update()
|
._default_manager.select_for_update()
|
||||||
.select_related("device_type", "netbox_utilities_rack_placement")
|
.select_related("device_type")
|
||||||
.filter(rack=rack, position__isnull=False)
|
.filter(rack=rack, position__isnull=False)
|
||||||
.exclude(pk=device.pk)
|
.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 = []
|
conflicts = []
|
||||||
for candidate in candidates:
|
for candidate in candidates:
|
||||||
candidate_height, candidate_full_depth = _device_footprint(candidate)
|
candidate_height, candidate_full_depth = _device_footprint(candidate)
|
||||||
if not (full_depth or candidate_full_depth or candidate.face == face):
|
if not (full_depth or candidate_full_depth or candidate.face == face):
|
||||||
continue
|
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(
|
if placement_rectangles_overlap(
|
||||||
position,
|
position,
|
||||||
height,
|
height,
|
||||||
|
|||||||
@@ -18,10 +18,15 @@ from netbox_utilities.netbox_export import (
|
|||||||
|
|
||||||
|
|
||||||
class FakeQuerySet(list):
|
class FakeQuerySet(list):
|
||||||
|
def __init__(self, values=()):
|
||||||
|
super().__init__(values)
|
||||||
|
self.selected_related = []
|
||||||
|
|
||||||
def select_for_update(self):
|
def select_for_update(self):
|
||||||
return self
|
return self
|
||||||
|
|
||||||
def select_related(self, *args):
|
def select_related(self, *args):
|
||||||
|
self.selected_related.append(args)
|
||||||
return self
|
return self
|
||||||
|
|
||||||
def filter(self, **kwargs):
|
def filter(self, **kwargs):
|
||||||
@@ -29,7 +34,9 @@ class FakeQuerySet(list):
|
|||||||
|
|
||||||
def exclude(self, **kwargs):
|
def exclude(self, **kwargs):
|
||||||
pk = kwargs.get("pk")
|
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:
|
class FakeDevice:
|
||||||
@@ -111,17 +118,22 @@ class NetBoxExportRackWidthTest(SimpleTestCase):
|
|||||||
|
|
||||||
self.assertIs(_defer_rack_width(original, FakeDevice, record)[1], native)
|
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)
|
left = FakeDevice(1, width=2, horizontal_position=1, position=11)
|
||||||
right = FakeDevice(2, width=2, horizontal_position=2)
|
right = FakeDevice(2, width=2, horizontal_position=2)
|
||||||
right.rack_id = 1
|
right.rack_id = 1
|
||||||
FakeDevice._default_manager = FakeQuerySet([left])
|
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()
|
original = MagicMock()
|
||||||
|
|
||||||
conflicts = _width_aware_device_placement_conflicts(original, right, object(), 11, "front")
|
conflicts = _width_aware_device_placement_conflicts(original, right, object(), 11, "front")
|
||||||
|
|
||||||
self.assertEqual(conflicts, [])
|
self.assertEqual(conflicts, [])
|
||||||
original.assert_not_called()
|
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 = FakeDevice(3, width=2, horizontal_position=1)
|
||||||
overlapping.rack_id = 1
|
overlapping.rack_id = 1
|
||||||
|
|||||||
@@ -449,7 +449,7 @@ class ReorderRackFrontendTest(SimpleTestCase):
|
|||||||
self.assertEqual(template_name, "netbox_utilities/reorder_rack.html")
|
self.assertEqual(template_name, "netbox_utilities/reorder_rack.html")
|
||||||
self.assertEqual(context["reorder_devices"][0]["label"], "LEO-Fritzbox")
|
self.assertEqual(context["reorder_devices"][0]["label"], "LEO-Fritzbox")
|
||||||
self.assertEqual(context["reorder_devices"][0]["grid_width"], 6)
|
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)
|
self.assertIs(context["reorder_rack_width_data"], get_width_data.return_value)
|
||||||
get_width_data.assert_called_once()
|
get_width_data.assert_called_once()
|
||||||
self.assertIs(get_width_data.call_args.kwargs["rack"], rack)
|
self.assertIs(get_width_data.call_args.kwargs["rack"], rack)
|
||||||
|
|||||||
@@ -246,7 +246,7 @@ class TopologyViewsRackWidthTest(SimpleTestCase):
|
|||||||
self.assertIn('id="netbox-utilities-topology-rack-width-styles"', html)
|
self.assertIn('id="netbox-utilities-topology-rack-width-styles"', html)
|
||||||
self.assertIn('.rack-device[href="/dcim/devices/334/"]', html)
|
self.assertIn('.rack-device[href="/dcim/devices/334/"]', html)
|
||||||
self.assertIn("left: calc(50% + 3px) !important", 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(0% + 3px)!important", html)
|
||||||
self.assertIn("left:calc(50% + 3px)!important", html)
|
self.assertIn("left:calc(50% + 3px)!important", html)
|
||||||
self.assertIn("width:calc(50% - 6px)!important", html)
|
self.assertIn("width:calc(50% - 6px)!important", html)
|
||||||
|
|||||||
+1
-1
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "netbox-utilities"
|
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"
|
description = "Navigation, tenant utilities, partial-width rack devices, bulk uploads, and rack reordering for NetBox 4.6"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
requires-python = ">=3.12"
|
requires-python = ">=3.12"
|
||||||
|
|||||||
Reference in New Issue
Block a user