201 lines
6.7 KiB
Python
201 lines
6.7 KiB
Python
import logging
|
|
from dataclasses import dataclass
|
|
from decimal import Decimal
|
|
from functools import wraps
|
|
from importlib import import_module
|
|
|
|
from django.apps import apps
|
|
from django.core.exceptions import ValidationError
|
|
|
|
from .models import DeviceRackPlacement
|
|
from .rack_width import (
|
|
FULL_WIDTH,
|
|
get_width_position,
|
|
normalize_width_position,
|
|
placement_rectangles_overlap,
|
|
stage_width_position,
|
|
)
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
SUPPORTED_NETBOX_EXPORT_VERSIONS = {"0.3.11"}
|
|
RACK_WIDTH_RECORD_KEY = "netbox_utilities_rack_placement"
|
|
PATCH_MARKER = "_netbox_utilities_rack_width_version"
|
|
PATCH_VERSION = 1
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class WidthAwareDeferredDevicePlacement:
|
|
rack_spec: dict | None
|
|
position: object
|
|
face: object
|
|
width: int
|
|
horizontal_position: int
|
|
|
|
|
|
def _is_device(value):
|
|
return getattr(getattr(value, "_meta", None), "label_lower", None) == "dcim.device"
|
|
|
|
|
|
def _serialize_rack_width(original, obj, *args, **kwargs):
|
|
record = original(obj, *args, **kwargs)
|
|
if _is_device(obj):
|
|
width, horizontal_position = get_width_position(obj)
|
|
record[RACK_WIDTH_RECORD_KEY] = {
|
|
"schema_version": 1,
|
|
"width": width,
|
|
"horizontal_position": horizontal_position,
|
|
}
|
|
return record
|
|
|
|
|
|
def _defer_rack_width(original, model, record):
|
|
prepared, placement = original(model, record)
|
|
if placement is None:
|
|
return prepared, placement
|
|
|
|
metadata = record.get(RACK_WIDTH_RECORD_KEY)
|
|
if not isinstance(metadata, dict) or metadata.get("schema_version") != 1:
|
|
return prepared, placement
|
|
try:
|
|
width, horizontal_position = normalize_width_position(
|
|
metadata.get("width"),
|
|
metadata.get("horizontal_position"),
|
|
)
|
|
except ValidationError:
|
|
logger.warning("NetBox Export archive contains invalid rack width metadata for %s", record.get("id"))
|
|
return prepared, placement
|
|
|
|
return prepared, WidthAwareDeferredDevicePlacement(
|
|
rack_spec=placement.rack_spec,
|
|
position=placement.position,
|
|
face=placement.face,
|
|
width=width,
|
|
horizontal_position=horizontal_position,
|
|
)
|
|
|
|
|
|
def _device_footprint(device):
|
|
device_type = getattr(device, "device_type", None)
|
|
height = Decimal(str(getattr(device_type, "u_height", 1) or 0))
|
|
if height <= 0:
|
|
height = Decimal("0.5")
|
|
return height, bool(getattr(device_type, "is_full_depth", False))
|
|
|
|
|
|
def _width_aware_device_placement_conflicts(original, device, rack, position, face):
|
|
if not _is_device(device):
|
|
return original(device, rack, position, face)
|
|
if rack is None or position is None:
|
|
return []
|
|
|
|
width, horizontal_position = get_width_position(device)
|
|
height, full_depth = _device_footprint(device)
|
|
candidates = (
|
|
type(device)
|
|
._default_manager.select_for_update()
|
|
.select_related("device_type", "netbox_utilities_rack_placement")
|
|
.filter(rack=rack, position__isnull=False)
|
|
.exclude(pk=device.pk)
|
|
)
|
|
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)
|
|
if placement_rectangles_overlap(
|
|
position,
|
|
height,
|
|
width,
|
|
horizontal_position,
|
|
candidate.position,
|
|
candidate_height,
|
|
candidate_width,
|
|
candidate_horizontal_position,
|
|
):
|
|
conflicts.append(candidate)
|
|
return conflicts
|
|
|
|
|
|
def _apply_rack_widths(original, placements, resolved, resolver, compatibility, **kwargs):
|
|
placements = list(placements)
|
|
width_aware = []
|
|
for record_id, placement in placements:
|
|
if not isinstance(placement, WidthAwareDeferredDevicePlacement):
|
|
continue
|
|
device = resolved[record_id]
|
|
stage_width_position(device, placement.width, placement.horizontal_position)
|
|
width_aware.append((device, placement))
|
|
|
|
result = original(placements, resolved, resolver, compatibility, **kwargs)
|
|
|
|
for device, placement in width_aware:
|
|
if placement.width == FULL_WIDTH or not device.rack_id or not device.position:
|
|
DeviceRackPlacement.objects.filter(device=device).delete()
|
|
else:
|
|
DeviceRackPlacement.objects.update_or_create(
|
|
device=device,
|
|
defaults={
|
|
"width": placement.width,
|
|
"horizontal_position": placement.horizontal_position,
|
|
},
|
|
)
|
|
return result
|
|
|
|
|
|
def _rack_placement_is_exportable(original, model):
|
|
if model is DeviceRackPlacement:
|
|
return True
|
|
return original(model)
|
|
|
|
|
|
def _patch_function(module, name, wrapper_factory):
|
|
current = getattr(module, name)
|
|
if getattr(current, PATCH_MARKER, None) == PATCH_VERSION:
|
|
return
|
|
original = current
|
|
|
|
@wraps(original)
|
|
def patched(*args, **kwargs):
|
|
return wrapper_factory(original, *args, **kwargs)
|
|
|
|
setattr(patched, PATCH_MARKER, PATCH_VERSION)
|
|
setattr(module, name, patched)
|
|
|
|
|
|
def install_netbox_export_rack_width_support():
|
|
if not apps.is_installed("netbox_export"):
|
|
return False
|
|
|
|
plugin_config = apps.get_app_config("netbox_export")
|
|
plugin_version = getattr(plugin_config, "version", None)
|
|
if plugin_version not in SUPPORTED_NETBOX_EXPORT_VERSIONS:
|
|
logger.warning(
|
|
"NetBox Utilities did not patch netbox-export version %s; supported versions: %s",
|
|
plugin_version,
|
|
", ".join(sorted(SUPPORTED_NETBOX_EXPORT_VERSIONS)),
|
|
)
|
|
return False
|
|
|
|
exporter = import_module("netbox_export.services.exporter")
|
|
graph = import_module("netbox_export.services.graph")
|
|
importer = import_module("netbox_export.services.importer")
|
|
required = (
|
|
(exporter, "serialize_object"),
|
|
(graph, "is_exportable_model"),
|
|
(importer, "_defer_device_placement"),
|
|
(importer, "_device_placement_conflicts"),
|
|
(importer, "_apply_device_placements"),
|
|
)
|
|
if not all(hasattr(module, name) for module, name in required):
|
|
logger.warning("NetBox Utilities could not find the expected netbox-export 0.3.11 integration hooks")
|
|
return False
|
|
|
|
_patch_function(exporter, "serialize_object", _serialize_rack_width)
|
|
_patch_function(graph, "is_exportable_model", _rack_placement_is_exportable)
|
|
_patch_function(importer, "_defer_device_placement", _defer_rack_width)
|
|
_patch_function(importer, "_device_placement_conflicts", _width_aware_device_placement_conflicts)
|
|
_patch_function(importer, "_apply_device_placements", _apply_rack_widths)
|
|
return True
|