first commit
This commit is contained in:
498
netbox_librenms_plugin/views/base/ip_addresses_view.py
Normal file
498
netbox_librenms_plugin/views/base/ip_addresses_view.py
Normal 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)
|
||||
Reference in New Issue
Block a user