fix: preserve shared rack units during import
This commit is contained in:
@@ -287,6 +287,13 @@ Test- und Echtimport auch unter PostgreSQL, ohne einen unzulässigen
|
|||||||
`FOR UPDATE`-Outer-Join zu erzeugen. Bereits mit Version `0.9.9` erstellte
|
`FOR UPDATE`-Outer-Join zu erzeugen. Bereits mit Version `0.9.9` erstellte
|
||||||
Archive müssen dafür nicht neu exportiert werden.
|
Archive müssen dafür nicht neu exportiert werden.
|
||||||
|
|
||||||
|
Ab Version `0.9.11` werden alle im Archiv enthaltenen Rackbreiten vor der
|
||||||
|
eigentlichen Geräteplatzierung innerhalb derselben Transaktion vorgemerkt.
|
||||||
|
Dadurch erkennt die Konfliktprüfung mehrere teilbreite Geräte derselben HE
|
||||||
|
bereits während des Imports korrekt und löst kein zuvor platziertes Gerät aus
|
||||||
|
dem Rack. Nach der Platzierung werden die Breiten mit dem endgültigen
|
||||||
|
Importergebnis abgeglichen.
|
||||||
|
|
||||||
### 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.10"
|
__version__ = "0.9.11"
|
||||||
|
|
||||||
|
|
||||||
class NetBoxUtilitiesConfig(PluginConfig):
|
class NetBoxUtilitiesConfig(PluginConfig):
|
||||||
|
|||||||
@@ -141,12 +141,26 @@ def _apply_rack_widths(original, placements, resolved, resolver, compatibility,
|
|||||||
stage_width_position(device, placement.width, placement.horizontal_position)
|
stage_width_position(device, placement.width, placement.horizontal_position)
|
||||||
width_aware.append((device, placement))
|
width_aware.append((device, placement))
|
||||||
|
|
||||||
|
# The native placement pass reloads rack occupants from the database for
|
||||||
|
# every device. Persist all imported widths first so a device placed early
|
||||||
|
# in the same pass is not mistaken for a full-width conflict later on.
|
||||||
|
for device, placement in width_aware:
|
||||||
|
_save_rack_width(device, placement)
|
||||||
|
|
||||||
result = original(placements, resolved, resolver, compatibility, **kwargs)
|
result = original(placements, resolved, resolver, compatibility, **kwargs)
|
||||||
|
|
||||||
for device, placement in width_aware:
|
for device, placement in width_aware:
|
||||||
if placement.width == FULL_WIDTH or not device.rack_id or not device.position:
|
if placement.width == FULL_WIDTH or not device.rack_id or not device.position:
|
||||||
DeviceRackPlacement.objects.filter(device=device).delete()
|
DeviceRackPlacement.objects.filter(device=device).delete()
|
||||||
else:
|
else:
|
||||||
|
_save_rack_width(device, placement)
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def _save_rack_width(device, placement):
|
||||||
|
if placement.width == FULL_WIDTH:
|
||||||
|
DeviceRackPlacement.objects.filter(device=device).delete()
|
||||||
|
return
|
||||||
DeviceRackPlacement.objects.update_or_create(
|
DeviceRackPlacement.objects.update_or_create(
|
||||||
device=device,
|
device=device,
|
||||||
defaults={
|
defaults={
|
||||||
@@ -154,7 +168,6 @@ def _apply_rack_widths(original, placements, resolved, resolver, compatibility,
|
|||||||
"horizontal_position": placement.horizontal_position,
|
"horizontal_position": placement.horizontal_position,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
return result
|
|
||||||
|
|
||||||
|
|
||||||
def _rack_placement_is_exportable(original, model):
|
def _rack_placement_is_exportable(original, model):
|
||||||
|
|||||||
@@ -30,7 +30,12 @@ class FakeQuerySet(list):
|
|||||||
return self
|
return self
|
||||||
|
|
||||||
def filter(self, **kwargs):
|
def filter(self, **kwargs):
|
||||||
return self
|
values = self
|
||||||
|
if kwargs.get("position__isnull") is False:
|
||||||
|
values = [item for item in values if item.position is not None]
|
||||||
|
result = FakeQuerySet(values)
|
||||||
|
result.selected_related = self.selected_related
|
||||||
|
return result
|
||||||
|
|
||||||
def exclude(self, **kwargs):
|
def exclude(self, **kwargs):
|
||||||
pk = kwargs.get("pk")
|
pk = kwargs.get("pk")
|
||||||
@@ -155,6 +160,12 @@ class NetBoxExportRackWidthTest(SimpleTestCase):
|
|||||||
self.assertEqual(received, placements)
|
self.assertEqual(received, placements)
|
||||||
self.assertEqual(partial._netbox_utilities_rack_width, 2)
|
self.assertEqual(partial._netbox_utilities_rack_width, 2)
|
||||||
self.assertEqual(full._netbox_utilities_rack_width, 1)
|
self.assertEqual(full._netbox_utilities_rack_width, 1)
|
||||||
|
placement_objects.update_or_create.assert_called_once_with(
|
||||||
|
device=partial,
|
||||||
|
defaults={"width": 2, "horizontal_position": 1},
|
||||||
|
)
|
||||||
|
placement_objects.filter.assert_called_once_with(device=full)
|
||||||
|
placement_objects.filter.return_value.delete.assert_called_once_with()
|
||||||
partial.rack_id = full.rack_id = 1
|
partial.rack_id = full.rack_id = 1
|
||||||
partial.position = 11
|
partial.position = 11
|
||||||
full.position = 12
|
full.position = 12
|
||||||
@@ -163,12 +174,73 @@ class NetBoxExportRackWidthTest(SimpleTestCase):
|
|||||||
result = _apply_rack_widths(native_apply, placements, resolved, object(), object())
|
result = _apply_rack_widths(native_apply, placements, resolved, object(), object())
|
||||||
|
|
||||||
self.assertEqual(result, "saved")
|
self.assertEqual(result, "saved")
|
||||||
placement_objects.update_or_create.assert_called_once_with(
|
self.assertEqual(placement_objects.update_or_create.call_count, 2)
|
||||||
device=partial,
|
self.assertEqual(placement_objects.filter.call_count, 2)
|
||||||
defaults={"width": 2, "horizontal_position": 1},
|
self.assertEqual(placement_objects.filter.return_value.delete.call_count, 2)
|
||||||
|
|
||||||
|
@patch("netbox_utilities.netbox_export.DeviceRackPlacement.objects")
|
||||||
|
def test_import_preloads_all_partial_widths_before_native_placement(self, placement_objects):
|
||||||
|
left = FakeDevice(1)
|
||||||
|
right = FakeDevice(2)
|
||||||
|
placements = [
|
||||||
|
("left", WidthAwareDeferredDevicePlacement(None, 11, "front", 2, 1)),
|
||||||
|
("right", WidthAwareDeferredDevicePlacement(None, 11, "front", 2, 2)),
|
||||||
|
]
|
||||||
|
resolved = {"left": left, "right": right}
|
||||||
|
|
||||||
|
def native_apply(*_args, **_kwargs):
|
||||||
|
self.assertEqual(placement_objects.update_or_create.call_count, 2)
|
||||||
|
self.assertEqual(
|
||||||
|
[call.kwargs["device"] for call in placement_objects.update_or_create.call_args_list],
|
||||||
|
[left, right],
|
||||||
)
|
)
|
||||||
placement_objects.filter.assert_called_once_with(device=full)
|
left.rack_id = right.rack_id = 1
|
||||||
placement_objects.filter.return_value.delete.assert_called_once_with()
|
left.position = right.position = 11
|
||||||
|
|
||||||
|
_apply_rack_widths(native_apply, placements, resolved, object(), object())
|
||||||
|
|
||||||
|
self.assertEqual(placement_objects.update_or_create.call_count, 4)
|
||||||
|
|
||||||
|
@patch("netbox_utilities.netbox_export.DeviceRackPlacement.objects")
|
||||||
|
def test_two_partial_devices_remain_in_the_same_unit_during_import(self, placement_objects):
|
||||||
|
left = FakeDevice(1)
|
||||||
|
right = FakeDevice(2)
|
||||||
|
FakeDevice._default_manager = FakeQuerySet([left, right])
|
||||||
|
placements = [
|
||||||
|
("left", WidthAwareDeferredDevicePlacement(None, 11, "front", 2, 1)),
|
||||||
|
("right", WidthAwareDeferredDevicePlacement(None, 11, "front", 2, 2)),
|
||||||
|
]
|
||||||
|
resolved = {"left": left, "right": right}
|
||||||
|
stored = {}
|
||||||
|
|
||||||
|
def store_width(*, device, defaults):
|
||||||
|
stored[device.pk] = SimpleNamespace(device_id=device.pk, **defaults)
|
||||||
|
|
||||||
|
def locked_widths(**kwargs):
|
||||||
|
return [stored[pk] for pk in kwargs["device_id__in"] if pk in stored]
|
||||||
|
|
||||||
|
placement_objects.update_or_create.side_effect = store_width
|
||||||
|
placement_objects.select_for_update.return_value.filter.side_effect = locked_widths
|
||||||
|
|
||||||
|
def native_apply(received, *_args, **_kwargs):
|
||||||
|
for record_id, placement in received:
|
||||||
|
device = resolved[record_id]
|
||||||
|
conflicts = _width_aware_device_placement_conflicts(
|
||||||
|
MagicMock(),
|
||||||
|
device,
|
||||||
|
object(),
|
||||||
|
placement.position,
|
||||||
|
placement.face,
|
||||||
|
)
|
||||||
|
self.assertEqual(conflicts, [])
|
||||||
|
device.rack_id = 1
|
||||||
|
device.position = placement.position
|
||||||
|
device.face = placement.face
|
||||||
|
|
||||||
|
_apply_rack_widths(native_apply, placements, resolved, object(), object())
|
||||||
|
|
||||||
|
self.assertEqual(left.position, 11)
|
||||||
|
self.assertEqual(right.position, 11)
|
||||||
|
|
||||||
@patch("netbox_utilities.netbox_export.import_module")
|
@patch("netbox_utilities.netbox_export.import_module")
|
||||||
@patch("netbox_utilities.netbox_export.apps.get_app_config")
|
@patch("netbox_utilities.netbox_export.apps.get_app_config")
|
||||||
|
|||||||
@@ -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.10")
|
self.assertEqual(context["asset_version"], "0.9.11")
|
||||||
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.10", html)
|
self.assertIn("netbox_utilities/topology-rack-width.js?v=0.9.11", 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.10"
|
version = "0.9.11"
|
||||||
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