first commit
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

This commit is contained in:
Vlastislav Svatek
2026-06-05 10:39:05 +02:00
commit 673e67106e
217 changed files with 76612 additions and 0 deletions

View File

@@ -0,0 +1,498 @@
import json
from dcim.models import Device
from django.contrib import messages
from django.core.cache import cache
from django.http import Http404, JsonResponse
from django.shortcuts import get_object_or_404, render
from django.utils import timezone
from django.views import View
from ipam.models import VRF, IPAddress
from virtualization.models import VirtualMachine
from netbox_librenms_plugin.tables.ipaddresses import IPAddressTable
from netbox_librenms_plugin.utils import get_interface_name_field, get_librenms_device_id
from netbox_librenms_plugin.views.mixins import CacheMixin, LibreNMSAPIMixin, LibreNMSPermissionMixin
class BaseIPAddressTableView(LibreNMSPermissionMixin, LibreNMSAPIMixin, CacheMixin, View):
"""
Base view for synchronizing IP address information from LibreNMS.
"""
partial_template_name = "netbox_librenms_plugin/_ipaddress_sync_content.html"
interface_name_field = None
def get_object(self, pk):
return get_object_or_404(self.model, pk=pk)
def get_ip_addresses(self, obj):
"""Fetch IP address data from LibreNMS for the given object."""
self.librenms_id = self.librenms_api.get_librenms_id(obj)
return self.librenms_api.get_device_ips(self.librenms_id)
def enrich_ip_data(self, ip_data, obj, interface_name_field):
"""
Enrich IP data with NetBox information in a more efficient manner.
This optimized implementation:
1. Caches port data to reduce API calls
2. Pre-loads all relevant device data
3. Uses dictionary lookups instead of repeated iterations
"""
# Prefetch all necessary data
prefetched_data = self._prefetch_netbox_data(obj)
port_data_cache = {} # Cache for LibreNMS port data to minimize API calls
enriched_data = []
# Process each IP address from LibreNMS
for ip_entry in ip_data:
# Skip invalid entries that are not dictionaries
if not isinstance(ip_entry, dict):
continue
# Skip entries missing required fields
if "port_id" not in ip_entry:
continue
# Get or fetch port data (with caching)
port_info = self._get_port_info(ip_entry["port_id"], port_data_cache, interface_name_field)
# Create enriched IP structure with base data
enriched_ip = self._create_base_ip_entry(ip_entry, obj, prefetched_data["vrfs"])
# Get LibreNMS interface name if available
librenms_interface_name = None
if port_info:
librenms_interface_name = port_info.get(interface_name_field)
enriched_ip["interface_name"] = librenms_interface_name
# IP with mask is already calculated in _create_base_ip_entry
ip_with_mask = enriched_ip["ip_with_mask"]
ip_address = prefetched_data["ip_addresses_map"].get(ip_with_mask)
if ip_address:
# Process existing IP
self._enrich_existing_ip(
enriched_ip,
ip_address,
ip_entry["port_id"],
librenms_interface_name,
prefetched_data,
)
else:
# New IP that doesn't exist in NetBox
enriched_ip["exists"] = False
enriched_ip["status"] = "sync"
# Add interface information (regardless of IP status)
self._add_interface_info_to_ip(
enriched_ip,
ip_entry["port_id"],
librenms_interface_name,
prefetched_data,
)
enriched_data.append(enriched_ip)
return enriched_data
def _prefetch_netbox_data(self, obj):
"""Prefetch all necessary NetBox data to minimize database queries"""
# Get all interfaces for the device
all_interfaces = list(obj.interfaces.all())
# Create maps for efficient lookups
server_key = self.librenms_api.server_key
interfaces_by_librenms_id = {}
for interface in all_interfaces:
lib_id = get_librenms_device_id(interface, server_key, auto_save=False)
if lib_id is not None:
interfaces_by_librenms_id[str(lib_id)] = interface
interfaces_by_name = {interface.name: interface for interface in all_interfaces}
# Get all IP addresses
ip_addresses_map = {
str(ip.address): ip for ip in IPAddress.objects.select_related("assigned_object_type", "vrf")
}
# Get all VRFs
vrfs = list(VRF.objects.all())
return {
"interfaces_by_librenms_id": interfaces_by_librenms_id,
"interfaces_by_name": interfaces_by_name,
"all_interfaces": all_interfaces,
"device": obj,
"ip_addresses_map": ip_addresses_map,
"vrfs": vrfs,
}
def _get_port_info(self, port_id, port_data_cache, interface_name_field):
"""Get port info from LibreNMS with caching to minimize API calls"""
if port_id not in port_data_cache:
success, port_data = self.librenms_api.get_port_by_id(port_id)
if success and "port" in port_data and port_data["port"]:
port_data_cache[port_id] = port_data["port"][0]
else:
port_data_cache[port_id] = None
return port_data_cache[port_id]
def _create_base_ip_entry(self, ip_entry, obj, vrfs):
"""Create the base data structure for an IP entry"""
# Determine if this is an IPv4 or IPv6 address and create unified fields
if "ip_address" in ip_entry and "prefix_length" in ip_entry:
# Use unified format directly if available
ip_address = ip_entry["ip_address"]
prefix_length = ip_entry["prefix_length"]
else:
# Legacy format handling
if "ipv6_compressed" in ip_entry:
ip_address = ip_entry["ipv6_compressed"]
prefix_length = ip_entry["ipv6_prefixlen"]
elif "ipv4_address" in ip_entry:
ip_address = ip_entry["ipv4_address"]
prefix_length = ip_entry["ipv4_prefixlen"]
else:
raise KeyError("No valid IP address format found in LibreNMS data")
ip_with_mask = f"{ip_address}/{prefix_length}"
return {
"ip_address": ip_address,
"prefix_length": prefix_length,
"ip_with_mask": ip_with_mask,
"port_id": ip_entry["port_id"],
"device": obj.name,
"device_url": obj.get_absolute_url(),
"vrf_id": None,
"vrfs": vrfs,
}
def _enrich_existing_ip(self, enriched_ip, ip_address, port_id, librenms_interface_name, prefetched_data):
"""Add information for IP addresses that exist in NetBox"""
enriched_ip["ip_url"] = ip_address.get_absolute_url()
enriched_ip["exists"] = True
# Add VRF info if available
if ip_address.vrf:
enriched_ip["vrf_id"] = ip_address.vrf.pk
enriched_ip["vrf"] = ip_address.vrf.name
# Set initial status to update (will change to matched if criteria met)
enriched_ip["status"] = "update"
# Only proceed if IP is assigned to an object
if not ip_address.assigned_object:
return
assigned_interface = ip_address.assigned_object
# Check if interface matches by LibreNMS ID
if str(port_id) in prefetched_data["interfaces_by_librenms_id"]:
interface = prefetched_data["interfaces_by_librenms_id"][str(port_id)]
if assigned_interface == interface:
enriched_ip["status"] = "matched"
return
# Check if interface matches by name
if librenms_interface_name and assigned_interface.name == librenms_interface_name:
enriched_ip["status"] = "matched"
# Add interface information
enriched_ip["interface_name"] = assigned_interface.name
enriched_ip["interface_url"] = assigned_interface.get_absolute_url()
def _add_interface_info_to_ip(self, enriched_ip, port_id, librenms_interface_name, prefetched_data):
"""Add interface information to the IP entry regardless of IP status"""
# First try to match by LibreNMS ID (highest priority)
if str(port_id) in prefetched_data["interfaces_by_librenms_id"]:
interface = prefetched_data["interfaces_by_librenms_id"][str(port_id)]
enriched_ip["interface_name"] = interface.name
enriched_ip["interface_url"] = interface.get_absolute_url()
return
# Then try to match by interface name
if librenms_interface_name and librenms_interface_name in prefetched_data["interfaces_by_name"]:
interface = prefetched_data["interfaces_by_name"][librenms_interface_name]
# Don't overwrite the interface name from LibreNMS but do add the URL
enriched_ip["interface_url"] = interface.get_absolute_url()
def get_table(self, data, obj, request):
"""Get the table instance for the view."""
table = IPAddressTable(data)
server_key = self.librenms_api.server_key
table.htmx_url = f"{request.path}?tab=ipaddresses" + (f"&server_key={server_key}" if server_key else "")
return table
def _prepare_context(self, request, obj, interface_name_field, fetch_fresh=False):
"""Helper method to prepare the context data for IP address sync views."""
table = None
cache_expiry = None
server_key = self.librenms_api.server_key
if interface_name_field is None:
interface_name_field = get_interface_name_field(request)
if fetch_fresh:
success, ip_data = self.get_ip_addresses(obj)
else:
cached_ip_data = cache.get(self.get_cache_key(obj, "ip_addresses", server_key))
if cached_ip_data:
ip_data = cached_ip_data.get("ip_addresses", [])
else:
return None
# Enrich data in both cases to ensure current NetBox state
ip_data = self.enrich_ip_data(ip_data, obj, interface_name_field)
if fetch_fresh:
# Cache the fresh data after enrichment
cache.set(
self.get_cache_key(obj, "ip_addresses", server_key),
{"ip_addresses": ip_data},
timeout=self.librenms_api.cache_timeout,
)
# Calculate cache expiry
cache_ttl = cache.ttl(self.get_cache_key(obj, "ip_addresses", server_key))
if cache_ttl is not None and cache_ttl > 0:
cache_expiry = timezone.now() + timezone.timedelta(seconds=cache_ttl)
# Generate the table
table = self.get_table(ip_data, obj, request)
table.configure(request)
# Prepare and return the context
return {
"table": table,
"object": obj,
"cache_expiry": cache_expiry,
"server_key": server_key,
}
def get_context_data(self, request, obj):
"""Get the context data for the IP address sync view."""
interface_name_field = get_interface_name_field(request)
context = self._prepare_context(request, obj, interface_name_field, fetch_fresh=False)
if context is None:
# No data found; return context with empty table
context = {"table": None, "object": obj, "cache_expiry": None, "server_key": self.librenms_api.server_key}
return context
def post(self, request, pk):
"""Handle POST request for IP address sync view."""
obj = self.get_object(pk)
interface_name_field = get_interface_name_field(request)
context = self._prepare_context(request, obj, interface_name_field, fetch_fresh=True)
if context is None:
messages.error(request, "No IP addresses found in LibreNMS")
return render(
request,
self.partial_template_name,
{
"ip_sync": {
"object": obj,
"table": None,
"cache_expiry": None,
"server_key": self.librenms_api.server_key,
}
},
)
messages.success(request, "IP address data refreshed successfully.")
return render(
request,
self.partial_template_name,
{"ip_sync": context},
)
class SingleIPAddressVerifyView(LibreNMSPermissionMixin, CacheMixin, View):
"""
View for verifying single IP address data with different VRF.
"""
def _get_object(self, object_id, object_type=None):
"""
Retrieve the object (Device or VirtualMachine) based on ID and optional type.
If type is not provided, tries to determine it by checking both Device and VM models.
"""
if object_type == "device":
return get_object_or_404(Device, pk=object_id)
elif object_type == "virtualmachine":
return get_object_or_404(VirtualMachine, pk=object_id)
else:
# Try to find object without knowing its type
obj = Device.objects.filter(pk=object_id).first()
if obj:
return obj
obj = VirtualMachine.objects.filter(pk=object_id).first()
if obj:
return obj
raise Http404(f"Object with ID {object_id} not found in Device or VirtualMachine models")
def _parse_ip_address(self, ip_address):
"""
Parse IP address string into address and prefix length.
Works with both IPv4 and IPv6 addresses.
"""
ip_address_parts = ip_address.split("/")
address_no_mask = ip_address_parts[0].strip()
if len(ip_address_parts) > 1:
try:
prefix_len = int(ip_address_parts[1])
return address_no_mask, prefix_len
except ValueError:
raise ValueError(f"Invalid prefix length: {ip_address_parts[1]}")
else:
raise ValueError("Prefix length is missing from the IP address")
def _find_in_cache(self, cached_data, address, prefix_len):
"""Find IP address in cache data using unified fields only."""
if not cached_data:
return None, None, None
for ip_entry in cached_data.get("ip_addresses", []):
if ip_entry["ip_address"] == address and str(ip_entry["prefix_length"]) == str(prefix_len):
return (ip_entry, ip_entry.get("vrf_id"), ip_entry.get("port_id"))
return None, None, None
def _find_existing_ip(self, address_no_mask, prefix_len, vrf_id=None):
"""
Find existing IP address in NetBox, optionally with specific VRF.
"""
ip_with_mask = f"{address_no_mask}/{prefix_len}"
# Check if IP exists in any VRF
existing_ip = IPAddress.objects.filter(address=ip_with_mask).first()
if not existing_ip:
return False, False, None
# IP exists in some VRF, check if it exists in the specified VRF
if vrf_id is not None:
existing_in_vrf = IPAddress.objects.filter(address=ip_with_mask, vrf__id=vrf_id).exists()
else:
# Check for global VRF (None)
existing_in_vrf = IPAddress.objects.filter(address=ip_with_mask, vrf__isnull=True).exists()
return True, existing_in_vrf, existing_ip.get_absolute_url()
def _determine_status(self, exists_any_vrf, exists_specific_vrf, original_vrf_id, vrf_id):
"""
Determine the status of an IP address based on existence and VRF.
"""
if exists_any_vrf:
# IP exists in NetBox
if exists_specific_vrf:
return "matched"
else:
return "update"
else:
# IP doesn't exist in NetBox, check if restoring to original VRF
if original_vrf_id is not None and original_vrf_id == vrf_id:
return "matched"
else:
return "sync"
def post(self, request):
"""
POST request to return json response with formatted IP address status.
"""
try:
try:
data = json.loads(request.body)
except json.JSONDecodeError as e:
return JsonResponse({"status": "error", "message": f"Invalid JSON: {e}"}, status=400)
ip_address = data.get("ip_address")
vrf_id = data.get("vrf_id")
object_id = data.get("device_id")
object_type = data.get("object_type")
server_key = data.get("server_key") or "default"
if not ip_address:
return JsonResponse({"status": "error", "message": "No IP address provided"}, status=400)
if not object_id:
return JsonResponse({"status": "error", "message": "No object ID provided"}, status=400)
# Get the object (Device or VirtualMachine)
try:
obj = self._get_object(object_id, object_type)
except Http404 as e:
return JsonResponse({"status": "error", "message": str(e)}, status=404)
# Parse IP address
try:
address_no_mask, prefix_len = self._parse_ip_address(ip_address)
except ValueError as e:
return JsonResponse({"status": "error", "message": str(e)}, status=400)
cache_key = self.get_cache_key(obj, "ip_addresses", server_key)
cached_data = cache.get(cache_key)
# Basic record with default values
updated_record = {
"ip_address": address_no_mask,
"prefix_length": prefix_len,
"ip_with_mask": f"{address_no_mask}/{prefix_len}",
"device": obj.name,
"device_url": obj.get_absolute_url(),
"vrf_id": vrf_id,
"exists": False,
"status": "sync",
}
# Try to find the IP in cache data
cache_entry, original_vrf_id, original_port_id = self._find_in_cache(
cached_data, address_no_mask, prefix_len
)
# Update record with cache data if found
if cache_entry:
# Update with all fields except vrf_id and status
for key, value in cache_entry.items():
if key not in ["vrf_id", "status"]:
updated_record[key] = value
# If no interface found in cache, use first device interface
if original_port_id is None:
interface = obj.interfaces.first()
if interface:
updated_record["interface_name"] = interface.name
updated_record["interface_url"] = interface.get_absolute_url()
# Check if IP exists in NetBox
exists_any_vrf, exists_specific_vrf, ip_url = self._find_existing_ip(address_no_mask, prefix_len, vrf_id)
if exists_any_vrf:
updated_record["exists"] = True
updated_record["ip_url"] = ip_url
# Determine status based on existence and VRF
updated_record["status"] = self._determine_status(
exists_any_vrf, exists_specific_vrf, original_vrf_id, vrf_id
)
# Render status HTML
table = IPAddressTable(data=[])
status_html = table.render_status(updated_record["status"], updated_record)
return JsonResponse(
{
"status": "success",
"ip_address": ip_address,
"formatted_row": {"status": status_html},
}
)
except Exception as e:
return JsonResponse({"status": "error", "message": str(e)}, status=500)