Files
netbox-librenms-plugin/netbox_librenms_plugin/views/sync/device_fields.py
Vlastislav Svatek 673e67106e
Some checks failed
ci / deploy (push) Has been cancelled
CodeQL Advanced / Analyze (actions) (push) Has been cancelled
CodeQL Advanced / Analyze (javascript-typescript) (push) Has been cancelled
CodeQL Advanced / Analyze (python) (push) Has been cancelled
first commit
2026-06-05 10:39:05 +02:00

714 lines
30 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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)