first commit
This commit is contained in:
713
netbox_librenms_plugin/views/sync/device_fields.py
Normal file
713
netbox_librenms_plugin/views/sync/device_fields.py
Normal file
@@ -0,0 +1,713 @@
|
||||
import logging
|
||||
|
||||
from dcim.models import Device, Manufacturer, Platform
|
||||
from django.contrib import messages
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.db import IntegrityError, transaction
|
||||
|
||||
from django.http import HttpResponse
|
||||
from django.shortcuts import get_object_or_404, redirect
|
||||
from django.views import View
|
||||
from virtualization.models import VirtualMachine
|
||||
|
||||
from netbox_librenms_plugin.import_utils import _determine_device_name
|
||||
from netbox_librenms_plugin.import_utils.virtual_chassis import _generate_vc_member_name
|
||||
from netbox_librenms_plugin.utils import (
|
||||
find_by_librenms_id,
|
||||
get_librenms_sync_device,
|
||||
match_librenms_hardware_to_device_type,
|
||||
migrate_legacy_librenms_id,
|
||||
resolve_naming_preferences,
|
||||
)
|
||||
from netbox_librenms_plugin.views.mixins import LibreNMSAPIMixin, LibreNMSPermissionMixin, NetBoxObjectPermissionMixin
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class UpdateDeviceNameView(LibreNMSPermissionMixin, NetBoxObjectPermissionMixin, LibreNMSAPIMixin, View):
|
||||
"""Update NetBox device name from LibreNMS sysName."""
|
||||
|
||||
required_object_permissions = {
|
||||
"POST": [("change", Device)],
|
||||
}
|
||||
|
||||
def post(self, request, pk):
|
||||
"""Sync the device name from LibreNMS sysName."""
|
||||
if error := self.require_all_permissions("POST"):
|
||||
return error
|
||||
|
||||
device = get_object_or_404(Device, pk=pk)
|
||||
|
||||
# For VC members without their own librenms_id, use the VC sync device
|
||||
librenms_lookup_device = device
|
||||
if hasattr(device, "virtual_chassis") and device.virtual_chassis:
|
||||
if not device.cf.get("librenms_id"):
|
||||
sync_device = get_librenms_sync_device(device)
|
||||
if sync_device:
|
||||
librenms_lookup_device = sync_device
|
||||
|
||||
self.librenms_id = self.librenms_api.get_librenms_id(librenms_lookup_device)
|
||||
|
||||
if not self.librenms_id:
|
||||
messages.error(request, "Device not found in LibreNMS")
|
||||
return redirect("plugins:netbox_librenms_plugin:device_librenms_sync", pk=pk)
|
||||
|
||||
success, device_info = self.librenms_api.get_device_info(self.librenms_id)
|
||||
|
||||
if not success or not device_info:
|
||||
messages.error(request, "Failed to retrieve device info from LibreNMS")
|
||||
return redirect("plugins:netbox_librenms_plugin:device_librenms_sync", pk=pk)
|
||||
|
||||
# Bail out early when LibreNMS has no usable name – the fallback
|
||||
# names that _determine_device_name generates (e.g. "device-42")
|
||||
# are only useful during import, not for renaming an existing device.
|
||||
if not (device_info.get("sysName") or device_info.get("hostname")):
|
||||
messages.warning(request, "No name could be determined from LibreNMS")
|
||||
return redirect("plugins:netbox_librenms_plugin:device_librenms_sync", pk=pk)
|
||||
|
||||
use_sysname, strip_domain = resolve_naming_preferences(request)
|
||||
|
||||
resolved_name = _determine_device_name(
|
||||
device_info,
|
||||
use_sysname=use_sysname,
|
||||
strip_domain=strip_domain,
|
||||
)
|
||||
|
||||
# For VC members, generate the expected VC member name
|
||||
if (
|
||||
resolved_name
|
||||
and hasattr(device, "virtual_chassis")
|
||||
and device.virtual_chassis is not None
|
||||
and device.vc_position is not None
|
||||
):
|
||||
resolved_name = _generate_vc_member_name(
|
||||
resolved_name,
|
||||
device.vc_position,
|
||||
serial=getattr(device, "serial", None),
|
||||
)
|
||||
|
||||
if not resolved_name:
|
||||
messages.warning(request, "No name could be determined from LibreNMS")
|
||||
return redirect("plugins:netbox_librenms_plugin:device_librenms_sync", pk=pk)
|
||||
|
||||
old_name = device.name
|
||||
device.name = resolved_name
|
||||
try:
|
||||
device.full_clean()
|
||||
device.save()
|
||||
except (ValidationError, IntegrityError) as e:
|
||||
device.name = old_name
|
||||
error_msg = e.message_dict if hasattr(e, "message_dict") else str(e)
|
||||
messages.error(request, f"Failed to update device name to '{resolved_name}': {error_msg}")
|
||||
return redirect("plugins:netbox_librenms_plugin:device_librenms_sync", pk=pk)
|
||||
|
||||
messages.success(request, f"Device name updated from '{old_name}' to '{resolved_name}'")
|
||||
|
||||
return redirect("plugins:netbox_librenms_plugin:device_librenms_sync", pk=pk)
|
||||
|
||||
|
||||
class UpdateDeviceSerialView(LibreNMSPermissionMixin, NetBoxObjectPermissionMixin, LibreNMSAPIMixin, View):
|
||||
"""Update NetBox device serial number from LibreNMS."""
|
||||
|
||||
required_object_permissions = {
|
||||
"POST": [("change", Device)],
|
||||
}
|
||||
|
||||
def post(self, request, pk):
|
||||
"""Sync the device serial number from LibreNMS."""
|
||||
# Check both plugin write and NetBox object permissions
|
||||
if error := self.require_all_permissions("POST"):
|
||||
return error
|
||||
|
||||
device = get_object_or_404(Device, pk=pk)
|
||||
self.librenms_id = self.librenms_api.get_librenms_id(device)
|
||||
|
||||
if not self.librenms_id:
|
||||
messages.error(request, "Device not found in LibreNMS")
|
||||
return redirect("plugins:netbox_librenms_plugin:device_librenms_sync", pk=pk)
|
||||
|
||||
success, device_info = self.librenms_api.get_device_info(self.librenms_id)
|
||||
|
||||
if not success or not device_info:
|
||||
messages.error(request, "Failed to retrieve device info from LibreNMS")
|
||||
return redirect("plugins:netbox_librenms_plugin:device_librenms_sync", pk=pk)
|
||||
|
||||
serial = device_info.get("serial")
|
||||
|
||||
if not serial or serial == "-":
|
||||
messages.warning(request, "No serial number available in LibreNMS")
|
||||
return redirect("plugins:netbox_librenms_plugin:device_librenms_sync", pk=pk)
|
||||
|
||||
old_serial = device.serial
|
||||
device.serial = serial
|
||||
try:
|
||||
device.full_clean()
|
||||
device.save()
|
||||
except (ValidationError, IntegrityError) as e:
|
||||
device.serial = old_serial
|
||||
error_msg = e.message_dict if hasattr(e, "message_dict") else str(e)
|
||||
messages.error(request, f"Failed to update serial to '{serial}': {error_msg}")
|
||||
return redirect("plugins:netbox_librenms_plugin:device_librenms_sync", pk=pk)
|
||||
|
||||
if old_serial:
|
||||
messages.success(
|
||||
request,
|
||||
f"Device serial updated from '{old_serial}' to '{serial}'",
|
||||
)
|
||||
else:
|
||||
messages.success(request, f"Device serial set to '{serial}'")
|
||||
|
||||
return redirect("plugins:netbox_librenms_plugin:device_librenms_sync", pk=pk)
|
||||
|
||||
|
||||
class UpdateDeviceTypeView(LibreNMSPermissionMixin, NetBoxObjectPermissionMixin, LibreNMSAPIMixin, View):
|
||||
"""Update NetBox DeviceType using LibreNMS hardware metadata."""
|
||||
|
||||
required_object_permissions = {
|
||||
"POST": [("change", Device)],
|
||||
}
|
||||
|
||||
def post(self, request, pk):
|
||||
"""Sync the device type from LibreNMS hardware info."""
|
||||
# Check both plugin write and NetBox object permissions
|
||||
if error := self.require_all_permissions("POST"):
|
||||
return error
|
||||
|
||||
device = get_object_or_404(Device, pk=pk)
|
||||
self.librenms_id = self.librenms_api.get_librenms_id(device)
|
||||
|
||||
if not self.librenms_id:
|
||||
messages.error(request, "Device not found in LibreNMS")
|
||||
return redirect("plugins:netbox_librenms_plugin:device_librenms_sync", pk=pk)
|
||||
|
||||
success, device_info = self.librenms_api.get_device_info(self.librenms_id)
|
||||
|
||||
if not success or not device_info:
|
||||
messages.error(request, "Failed to retrieve device info from LibreNMS")
|
||||
return redirect("plugins:netbox_librenms_plugin:device_librenms_sync", pk=pk)
|
||||
|
||||
hardware = device_info.get("hardware")
|
||||
|
||||
if not hardware:
|
||||
messages.warning(request, "No hardware information available in LibreNMS")
|
||||
return redirect("plugins:netbox_librenms_plugin:device_librenms_sync", pk=pk)
|
||||
|
||||
match_result = match_librenms_hardware_to_device_type(hardware)
|
||||
|
||||
if not match_result or not match_result["matched"]:
|
||||
messages.error(
|
||||
request,
|
||||
f"No matching DeviceType found for hardware '{hardware}'",
|
||||
)
|
||||
return redirect("plugins:netbox_librenms_plugin:device_librenms_sync", pk=pk)
|
||||
|
||||
device_type = match_result["device_type"]
|
||||
old_device_type = device.device_type
|
||||
device.device_type = device_type
|
||||
try:
|
||||
device.full_clean()
|
||||
device.save()
|
||||
except (ValidationError, IntegrityError) as e:
|
||||
device.device_type = old_device_type
|
||||
error_msg = e.message_dict if hasattr(e, "message_dict") else str(e)
|
||||
messages.error(request, f"Failed to update device type to '{device_type}': {error_msg}")
|
||||
return redirect("plugins:netbox_librenms_plugin:device_librenms_sync", pk=pk)
|
||||
|
||||
messages.success(
|
||||
request,
|
||||
f"Device type updated from '{old_device_type}' to '{device_type}'",
|
||||
)
|
||||
|
||||
return redirect("plugins:netbox_librenms_plugin:device_librenms_sync", pk=pk)
|
||||
|
||||
|
||||
class UpdateDevicePlatformView(LibreNMSPermissionMixin, NetBoxObjectPermissionMixin, LibreNMSAPIMixin, View):
|
||||
"""Update NetBox Platform based on LibreNMS OS info."""
|
||||
|
||||
required_object_permissions = {
|
||||
"POST": [("change", Device)],
|
||||
}
|
||||
|
||||
def post(self, request, pk):
|
||||
"""Sync the device platform from LibreNMS OS name."""
|
||||
# Check both plugin write and NetBox object permissions
|
||||
if error := self.require_all_permissions("POST"):
|
||||
return error
|
||||
|
||||
device = get_object_or_404(Device, pk=pk)
|
||||
self.librenms_id = self.librenms_api.get_librenms_id(device)
|
||||
|
||||
if not self.librenms_id:
|
||||
messages.error(request, "Device not found in LibreNMS")
|
||||
return redirect("plugins:netbox_librenms_plugin:device_librenms_sync", pk=pk)
|
||||
|
||||
success, device_info = self.librenms_api.get_device_info(self.librenms_id)
|
||||
|
||||
if not success or not device_info:
|
||||
messages.error(request, "Failed to retrieve device info from LibreNMS")
|
||||
return redirect("plugins:netbox_librenms_plugin:device_librenms_sync", pk=pk)
|
||||
|
||||
os_name = device_info.get("os")
|
||||
|
||||
if not os_name:
|
||||
messages.warning(request, "No OS information available in LibreNMS")
|
||||
return redirect("plugins:netbox_librenms_plugin:device_librenms_sync", pk=pk)
|
||||
|
||||
platform_name = os_name
|
||||
|
||||
try:
|
||||
platform = Platform.objects.get(name__iexact=platform_name)
|
||||
except Platform.DoesNotExist:
|
||||
messages.error(
|
||||
request,
|
||||
"Platform '{}' does not exist in NetBox. Use 'Create & Sync' button to create it first.".format(
|
||||
platform_name
|
||||
),
|
||||
)
|
||||
return redirect("plugins:netbox_librenms_plugin:device_librenms_sync", pk=pk)
|
||||
|
||||
old_platform = device.platform
|
||||
device.platform = platform
|
||||
try:
|
||||
device.full_clean()
|
||||
device.save()
|
||||
except (ValidationError, IntegrityError) as e:
|
||||
device.platform = old_platform
|
||||
error_msg = e.message_dict if hasattr(e, "message_dict") else str(e)
|
||||
messages.error(request, f"Failed to update platform to '{platform}': {error_msg}")
|
||||
return redirect("plugins:netbox_librenms_plugin:device_librenms_sync", pk=pk)
|
||||
|
||||
if old_platform:
|
||||
messages.success(
|
||||
request,
|
||||
f"Device platform updated from '{old_platform}' to '{platform}'",
|
||||
)
|
||||
else:
|
||||
messages.success(request, f"Device platform set to '{platform}'")
|
||||
|
||||
return redirect("plugins:netbox_librenms_plugin:device_librenms_sync", pk=pk)
|
||||
|
||||
|
||||
class CreateAndAssignPlatformView(LibreNMSPermissionMixin, NetBoxObjectPermissionMixin, LibreNMSAPIMixin, View):
|
||||
"""Create a new Platform and assign it to the device."""
|
||||
|
||||
required_object_permissions = {
|
||||
"POST": [
|
||||
("change", Device),
|
||||
("add", Platform),
|
||||
],
|
||||
}
|
||||
|
||||
def post(self, request, pk):
|
||||
"""Create a new platform and assign it to the device."""
|
||||
# Check both plugin write and NetBox object permissions
|
||||
if error := self.require_all_permissions("POST"):
|
||||
return error
|
||||
|
||||
device = get_object_or_404(Device, pk=pk)
|
||||
|
||||
platform_name = request.POST.get("platform_name")
|
||||
manufacturer_id = request.POST.get("manufacturer")
|
||||
|
||||
if not platform_name:
|
||||
messages.error(request, "Platform name is required")
|
||||
return redirect("plugins:netbox_librenms_plugin:device_librenms_sync", pk=pk)
|
||||
|
||||
if Platform.objects.filter(name__iexact=platform_name).exists():
|
||||
messages.warning(
|
||||
request,
|
||||
f"Platform '{platform_name}' already exists. Use the regular sync button.",
|
||||
)
|
||||
return redirect("plugins:netbox_librenms_plugin:device_librenms_sync", pk=pk)
|
||||
|
||||
manufacturer = None
|
||||
if manufacturer_id:
|
||||
try:
|
||||
manufacturer = Manufacturer.objects.get(pk=manufacturer_id)
|
||||
except Manufacturer.DoesNotExist:
|
||||
pass
|
||||
|
||||
with transaction.atomic():
|
||||
try:
|
||||
platform = Platform(
|
||||
name=platform_name,
|
||||
manufacturer=manufacturer,
|
||||
)
|
||||
platform.full_clean()
|
||||
platform.save()
|
||||
except ValidationError as e:
|
||||
transaction.set_rollback(True)
|
||||
error_msg = e.message_dict if hasattr(e, "message_dict") else str(e)
|
||||
logger.exception(
|
||||
"ValidationError creating platform '%s' for device pk=%s: %s",
|
||||
platform_name,
|
||||
pk,
|
||||
error_msg,
|
||||
)
|
||||
messages.error(
|
||||
request,
|
||||
f"Platform '{platform_name}' could not be created: {error_msg}",
|
||||
)
|
||||
return redirect("plugins:netbox_librenms_plugin:device_librenms_sync", pk=pk)
|
||||
except IntegrityError as e:
|
||||
transaction.set_rollback(True)
|
||||
logger.exception(
|
||||
"IntegrityError creating platform '%s' for device pk=%s",
|
||||
platform_name,
|
||||
pk,
|
||||
)
|
||||
messages.error(
|
||||
request,
|
||||
f"Platform '{platform_name}' could not be created: {e}",
|
||||
)
|
||||
return redirect("plugins:netbox_librenms_plugin:device_librenms_sync", pk=pk)
|
||||
|
||||
try:
|
||||
device = Device.objects.select_for_update().get(pk=pk)
|
||||
except Device.DoesNotExist:
|
||||
transaction.set_rollback(True)
|
||||
messages.error(request, "Device no longer exists.")
|
||||
return redirect("plugins:netbox_librenms_plugin:device_librenms_sync", pk=pk)
|
||||
|
||||
device.platform = platform
|
||||
try:
|
||||
device.full_clean()
|
||||
except ValidationError as e:
|
||||
transaction.set_rollback(True)
|
||||
error_msg = e.message_dict if hasattr(e, "message_dict") else str(e)
|
||||
logger.exception(
|
||||
"ValidationError validating device pk=%s: %s",
|
||||
pk,
|
||||
error_msg,
|
||||
)
|
||||
messages.error(
|
||||
request,
|
||||
f"Device (pk={pk}) validation failed: {error_msg}",
|
||||
)
|
||||
return redirect("plugins:netbox_librenms_plugin:device_librenms_sync", pk=pk)
|
||||
try:
|
||||
device.save()
|
||||
except IntegrityError as e:
|
||||
transaction.set_rollback(True)
|
||||
logger.exception("IntegrityError saving device pk=%s after platform assignment", pk)
|
||||
messages.error(
|
||||
request,
|
||||
f"Error saving device (pk={pk}): {e}",
|
||||
)
|
||||
return redirect("plugins:netbox_librenms_plugin:device_librenms_sync", pk=pk)
|
||||
|
||||
messages.success(
|
||||
request,
|
||||
f"Created platform '{platform}' and assigned to device",
|
||||
)
|
||||
|
||||
return redirect("plugins:netbox_librenms_plugin:device_librenms_sync", pk=pk)
|
||||
|
||||
|
||||
class AssignVCSerialView(LibreNMSPermissionMixin, NetBoxObjectPermissionMixin, LibreNMSAPIMixin, View):
|
||||
"""Assign serial numbers to each virtual chassis member."""
|
||||
|
||||
required_object_permissions = {
|
||||
"POST": [("change", Device)],
|
||||
}
|
||||
|
||||
def post(self, request, pk):
|
||||
"""Sync serial numbers to virtual chassis member devices."""
|
||||
# Check both plugin write and NetBox object permissions
|
||||
if error := self.require_all_permissions("POST"):
|
||||
return error
|
||||
|
||||
device = get_object_or_404(Device, pk=pk)
|
||||
|
||||
if not device.virtual_chassis:
|
||||
messages.error(request, "Device is not part of a virtual chassis")
|
||||
return redirect("plugins:netbox_librenms_plugin:device_librenms_sync", pk=pk)
|
||||
|
||||
assignments_made = 0
|
||||
errors = []
|
||||
|
||||
counter = 1
|
||||
while f"serial_{counter}" in request.POST:
|
||||
serial = request.POST.get(f"serial_{counter}")
|
||||
member_id = request.POST.get(f"member_id_{counter}")
|
||||
|
||||
if not member_id:
|
||||
counter += 1
|
||||
continue
|
||||
|
||||
try:
|
||||
member = Device.objects.get(pk=member_id)
|
||||
|
||||
if not member.virtual_chassis or member.virtual_chassis.pk != device.virtual_chassis.pk:
|
||||
errors.append(f"{member.name} is not part of the same virtual chassis")
|
||||
counter += 1
|
||||
continue
|
||||
|
||||
old_serial = member.serial
|
||||
member.serial = serial
|
||||
try:
|
||||
member.full_clean()
|
||||
member.save()
|
||||
except (ValidationError, IntegrityError) as e:
|
||||
member.serial = old_serial
|
||||
error_msg = e.message_dict if hasattr(e, "message_dict") else str(e)
|
||||
errors.append(f"Failed to set serial on {member.name}: {error_msg}")
|
||||
counter += 1
|
||||
continue
|
||||
|
||||
assignments_made += 1
|
||||
|
||||
except Device.DoesNotExist:
|
||||
errors.append(f"Device with ID {member_id} not found")
|
||||
except Exception as exc: # pragma: no cover - defensive guard
|
||||
errors.append(f"Error assigning serial to member {member_id}: {str(exc)}")
|
||||
|
||||
counter += 1
|
||||
|
||||
if assignments_made > 0:
|
||||
messages.success(
|
||||
request,
|
||||
f"Successfully assigned {assignments_made} serial number(s) to VC members",
|
||||
)
|
||||
|
||||
if errors:
|
||||
for error in errors:
|
||||
messages.error(request, error)
|
||||
|
||||
if assignments_made == 0 and not errors:
|
||||
messages.info(request, "No serial assignments were made")
|
||||
|
||||
return redirect("plugins:netbox_librenms_plugin:device_librenms_sync", pk=pk)
|
||||
|
||||
|
||||
class RemoveServerMappingView(LibreNMSPermissionMixin, NetBoxObjectPermissionMixin, View):
|
||||
"""Remove a single server entry from the device's (or VM's) librenms_id custom field dict."""
|
||||
|
||||
required_object_permissions = {
|
||||
"POST": [("change", Device), ("change", VirtualMachine)],
|
||||
}
|
||||
|
||||
def _get_object(self, object_type, pk):
|
||||
"""Return the Device or VirtualMachine for the given pk."""
|
||||
model = VirtualMachine if object_type == "vm" else Device
|
||||
return get_object_or_404(model, pk=pk), model
|
||||
|
||||
def _sync_url_name(self, object_type):
|
||||
if object_type == "vm":
|
||||
return "plugins:netbox_librenms_plugin:vm_librenms_sync"
|
||||
return "plugins:netbox_librenms_plugin:device_librenms_sync"
|
||||
|
||||
def _normalize_librenms_mapping(self, value):
|
||||
if isinstance(value, bool):
|
||||
return {}
|
||||
if isinstance(value, int):
|
||||
return {"default": value}
|
||||
if isinstance(value, str) and value.isdigit():
|
||||
return {"default": int(value)}
|
||||
return value if isinstance(value, dict) else {}
|
||||
|
||||
def post(self, request, pk):
|
||||
# Scope required permissions to the specific model being modified before checking.
|
||||
object_type = request.POST.get("object_type", "device")
|
||||
if object_type == "virtualmachine":
|
||||
object_type = "vm"
|
||||
if object_type not in ("device", "vm"):
|
||||
return HttpResponse(f"Invalid object_type: {object_type!r}", status=400)
|
||||
target_model = VirtualMachine if object_type == "vm" else Device
|
||||
self.required_object_permissions = {"POST": [("change", target_model)]}
|
||||
|
||||
if error := self.require_all_permissions("POST"):
|
||||
return error
|
||||
|
||||
obj, model = self._get_object(object_type, pk)
|
||||
sync_url = self._sync_url_name(object_type)
|
||||
server_key = request.POST.get("server_key", "").strip()
|
||||
|
||||
if not server_key:
|
||||
messages.error(request, "No server_key provided.")
|
||||
return redirect(sync_url, pk=pk)
|
||||
|
||||
cf_value = self._normalize_librenms_mapping(obj.custom_field_data.get("librenms_id"))
|
||||
if not isinstance(cf_value, dict) or server_key not in cf_value:
|
||||
messages.warning(request, f"No mapping found for server '{server_key}'.")
|
||||
return redirect(sync_url, pk=pk)
|
||||
|
||||
# Refuse to remove mappings for servers that are still configured in the plugin.
|
||||
# Only orphaned (unconfigured) mappings may be removed via this endpoint.
|
||||
# Guard both multi-server mode (servers dict) and legacy single-server mode
|
||||
# (top-level librenms_url in plugin config, which implicitly defines "default")
|
||||
# but only when no servers section is configured (pure legacy mode).
|
||||
from django.conf import settings as django_settings
|
||||
|
||||
plugins_cfg = django_settings.PLUGINS_CONFIG.get("netbox_librenms_plugin", {})
|
||||
configured_servers = plugins_cfg.get("servers") or {}
|
||||
if not isinstance(configured_servers, dict):
|
||||
configured_servers = {}
|
||||
legacy_url_configured = bool(plugins_cfg.get("librenms_url"))
|
||||
if server_key in configured_servers or (
|
||||
legacy_url_configured and not configured_servers and server_key == "default"
|
||||
):
|
||||
messages.error(
|
||||
request,
|
||||
f"Cannot remove mapping for configured server '{server_key}'. "
|
||||
"Remove the server from plugin configuration first, then retry.",
|
||||
)
|
||||
return redirect(sync_url, pk=pk)
|
||||
|
||||
with transaction.atomic():
|
||||
try:
|
||||
obj_locked = model.objects.select_for_update().get(pk=pk)
|
||||
except model.DoesNotExist:
|
||||
messages.error(request, f"{model.__name__} no longer exists.")
|
||||
return redirect(sync_url, pk=pk)
|
||||
cf = self._normalize_librenms_mapping(obj_locked.custom_field_data.get("librenms_id"))
|
||||
# Re-check after acquiring lock; mirror the pre-transaction protection logic
|
||||
_is_protected = server_key in configured_servers or (
|
||||
legacy_url_configured and not configured_servers and server_key == "default"
|
||||
)
|
||||
if isinstance(cf, dict) and server_key in cf and not _is_protected:
|
||||
del cf[server_key]
|
||||
obj_locked.custom_field_data["librenms_id"] = cf if cf else None
|
||||
try:
|
||||
obj_locked.full_clean()
|
||||
obj_locked.save()
|
||||
except ValidationError as exc:
|
||||
transaction.set_rollback(True)
|
||||
error_msg = exc.message_dict if hasattr(exc, "message_dict") else str(exc)
|
||||
logger.exception(
|
||||
"Validation error removing LibreNMS mapping for server %r: %s", server_key, error_msg
|
||||
)
|
||||
messages.error(request, f"Validation error removing LibreNMS mapping: {error_msg}")
|
||||
return redirect(sync_url, pk=pk)
|
||||
except Exception as exc:
|
||||
transaction.set_rollback(True)
|
||||
logger.exception("Unexpected error removing LibreNMS mapping for server %r", server_key)
|
||||
messages.error(request, f"Unexpected error removing LibreNMS mapping: {exc}")
|
||||
return redirect(sync_url, pk=pk)
|
||||
messages.success(request, f"Removed LibreNMS mapping for server '{server_key}'.")
|
||||
else:
|
||||
messages.warning(request, f"Mapping for server '{server_key}' was already removed.")
|
||||
|
||||
return redirect(sync_url, pk=pk)
|
||||
|
||||
|
||||
class ConvertLegacyLibreNMSIdView(LibreNMSPermissionMixin, NetBoxObjectPermissionMixin, LibreNMSAPIMixin, View):
|
||||
"""
|
||||
Convert a legacy bare-integer librenms_id to the server-scoped JSON dict format.
|
||||
|
||||
Only allowed when the NetBox serial matches the LibreNMS serial, so the
|
||||
association can be verified before scoping the ID to the active server.
|
||||
"""
|
||||
|
||||
required_object_permissions = {
|
||||
"POST": [("change", Device), ("change", VirtualMachine)],
|
||||
}
|
||||
|
||||
def _get_model_and_object(self, object_type, pk):
|
||||
model = VirtualMachine if object_type == "vm" else Device
|
||||
return model, get_object_or_404(model, pk=pk)
|
||||
|
||||
def _sync_url(self, object_type, pk):
|
||||
name = "vm_librenms_sync" if object_type == "vm" else "device_librenms_sync"
|
||||
return redirect(f"plugins:netbox_librenms_plugin:{name}", pk=pk)
|
||||
|
||||
def post(self, request, pk):
|
||||
object_type = request.POST.get("object_type", "device")
|
||||
if object_type == "virtualmachine":
|
||||
object_type = "vm"
|
||||
if object_type not in ("device", "vm"):
|
||||
return HttpResponse(f"Invalid object_type: {object_type!r}", status=400)
|
||||
|
||||
target_model = VirtualMachine if object_type == "vm" else Device
|
||||
self.required_object_permissions = {"POST": [("change", target_model)]}
|
||||
if error := self.require_all_permissions("POST"):
|
||||
return error
|
||||
|
||||
model, obj = self._get_model_and_object(object_type, pk)
|
||||
server_key = self.librenms_api.server_key
|
||||
|
||||
# Verify the device actually has a legacy bare-int librenms_id
|
||||
cf_value = obj.custom_field_data.get("librenms_id")
|
||||
if isinstance(cf_value, bool):
|
||||
messages.error(request, "librenms_id has an invalid boolean value; cannot convert.")
|
||||
return self._sync_url(object_type, pk)
|
||||
if not isinstance(cf_value, (int, str)):
|
||||
messages.warning(request, "librenms_id is already in the server-scoped JSON format.")
|
||||
return self._sync_url(object_type, pk)
|
||||
if isinstance(cf_value, str):
|
||||
if not cf_value.isdigit():
|
||||
messages.error(request, "librenms_id is not a valid integer; cannot convert.")
|
||||
return self._sync_url(object_type, pk)
|
||||
|
||||
# Verify serial match before converting
|
||||
librenms_id = int(cf_value) if isinstance(cf_value, str) else cf_value
|
||||
success, device_info = self.librenms_api.get_device_info(librenms_id)
|
||||
if not success or not device_info:
|
||||
messages.error(request, "Could not retrieve device info from LibreNMS to verify serial.")
|
||||
return self._sync_url(object_type, pk)
|
||||
|
||||
librenms_serial = (device_info.get("serial") or "").strip()
|
||||
netbox_serial = (getattr(obj, "serial", None) or "").strip()
|
||||
# VMs have no serial field in NetBox; skip the serial gate for them.
|
||||
is_vm = object_type == "vm"
|
||||
if not is_vm and (not netbox_serial or not librenms_serial or netbox_serial != librenms_serial):
|
||||
messages.error(
|
||||
request,
|
||||
"Serial number mismatch — cannot convert legacy ID without serial confirmation.",
|
||||
)
|
||||
return self._sync_url(object_type, pk)
|
||||
|
||||
with transaction.atomic():
|
||||
try:
|
||||
locked = model.objects.select_for_update().get(pk=pk)
|
||||
except model.DoesNotExist:
|
||||
messages.error(request, f"{model.__name__} no longer exists.")
|
||||
return self._sync_url(object_type, pk)
|
||||
# Re-check preconditions on the locked row (another admin may have
|
||||
# changed cf_value or serial between the initial read and the lock).
|
||||
locked_cf = locked.custom_field_data.get("librenms_id")
|
||||
if not isinstance(locked_cf, (int, str)) or isinstance(locked_cf, bool):
|
||||
messages.warning(request, "librenms_id is already in the server-scoped JSON format.")
|
||||
return self._sync_url(object_type, pk)
|
||||
locked_id = int(locked_cf) if isinstance(locked_cf, str) and locked_cf.isdigit() else locked_cf
|
||||
if not isinstance(locked_id, int):
|
||||
messages.error(request, "librenms_id changed before lock was acquired; aborting.")
|
||||
return self._sync_url(object_type, pk)
|
||||
locked_serial = (getattr(locked, "serial", None) or "").strip()
|
||||
if locked_id != librenms_id or locked_serial != netbox_serial:
|
||||
messages.error(request, "Device data changed before lock was acquired; aborting conversion.")
|
||||
return self._sync_url(object_type, pk)
|
||||
# Check that no other object already owns this ID (server-scoped or legacy)
|
||||
match = find_by_librenms_id(model, librenms_id, server_key)
|
||||
conflict = match is not None and match.pk != locked.pk
|
||||
if conflict:
|
||||
transaction.set_rollback(True)
|
||||
messages.error(
|
||||
request,
|
||||
f"Another {model.__name__} already has librenms_id {librenms_id} "
|
||||
f"for server '{server_key}'; cannot convert.",
|
||||
)
|
||||
return self._sync_url(object_type, pk)
|
||||
migrated = migrate_legacy_librenms_id(locked, server_key)
|
||||
if not migrated:
|
||||
messages.warning(request, "librenms_id is already in the server-scoped JSON format.")
|
||||
return self._sync_url(object_type, pk)
|
||||
try:
|
||||
locked.full_clean()
|
||||
locked.save()
|
||||
except ValidationError as exc:
|
||||
transaction.set_rollback(True)
|
||||
error_msg = exc.message_dict if hasattr(exc, "message_dict") else str(exc)
|
||||
messages.error(request, f"Failed to save converted librenms_id: {error_msg}")
|
||||
return self._sync_url(object_type, pk)
|
||||
except Exception as exc:
|
||||
transaction.set_rollback(True)
|
||||
logger.exception("Failed saving converted librenms_id for %s/%s", object_type, pk)
|
||||
messages.error(request, f"Failed to save converted librenms_id: {exc}")
|
||||
return self._sync_url(object_type, pk)
|
||||
|
||||
messages.success(
|
||||
request,
|
||||
f"Converted legacy librenms_id {librenms_id} → {{'{server_key}': {librenms_id}}}.",
|
||||
)
|
||||
return self._sync_url(object_type, pk)
|
||||
Reference in New Issue
Block a user