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,170 @@
from collections import namedtuple
from dcim.models import Site
from django.contrib import messages
from django.core.exceptions import ObjectDoesNotExist
from django.shortcuts import redirect
from django_tables2 import SingleTableView
from netbox_librenms_plugin.filtersets import SiteLocationFilterSet
from netbox_librenms_plugin.tables.locations import SiteLocationSyncTable
from netbox_librenms_plugin.views.mixins import LibreNMSAPIMixin, LibreNMSPermissionMixin
class SyncSiteLocationView(LibreNMSPermissionMixin, LibreNMSAPIMixin, SingleTableView):
"""Synchronize NetBox Sites with LibreNMS locations."""
table_class = SiteLocationSyncTable
template_name = "netbox_librenms_plugin/site_location_sync.html"
filterset = SiteLocationFilterSet
COORDINATE_TOLERANCE = 0.0001
SyncData = namedtuple("SyncData", ["netbox_site", "librenms_location", "is_synced"])
def get_table(self, *args, **kwargs):
"""Return the configured sync table."""
table = super().get_table(*args, **kwargs)
table.configure(self.request)
return table
def get_context_data(self, **kwargs):
"""Return context with filter form for site-location sync."""
context = super().get_context_data(**kwargs)
queryset = self.get_queryset()
context["filter_form"] = self.filterset(self.request.GET, queryset=queryset).form
return context
def get_queryset(self):
"""Return sync data pairing NetBox sites with LibreNMS locations."""
netbox_sites = Site.objects.all()
success, librenms_locations = self.get_librenms_locations()
if not success or not isinstance(librenms_locations, list):
return []
sync_data = [self.create_sync_data(site, librenms_locations) for site in netbox_sites]
if self.request.GET and self.filterset:
return self.filterset(self.request.GET, queryset=sync_data).qs
return sync_data
def get_librenms_locations(self):
"""Fetch all locations from LibreNMS."""
return self.librenms_api.get_locations()
def create_sync_data(self, site, librenms_locations):
"""Create a SyncData tuple pairing a site with its LibreNMS location."""
matched_location = self.match_site_with_location(site, librenms_locations)
if matched_location:
is_synced = self.check_coordinates_match(
site.latitude,
site.longitude,
matched_location.get("lat"),
matched_location.get("lng"),
)
return self.SyncData(site, matched_location, is_synced)
return self.SyncData(site, None, False)
def match_site_with_location(self, site, librenms_locations):
"""Return the LibreNMS location matching the given site, or None."""
for location in librenms_locations:
if location["location"].lower() == site.name.lower() or location["location"].lower() == site.slug.lower():
return location
return None
def check_coordinates_match(self, site_lat, site_lng, librenms_lat, librenms_lng):
"""Return True if site and LibreNMS coordinates match within tolerance."""
if None in (site_lat, site_lng, librenms_lat, librenms_lng):
return False
lat_match = abs(float(site_lat) - float(librenms_lat)) < self.COORDINATE_TOLERANCE
lng_match = abs(float(site_lng) - float(librenms_lng)) < self.COORDINATE_TOLERANCE
return lat_match and lng_match
def post(self, request):
"""Handle create or update of a LibreNMS location from a NetBox site."""
# Check write permission before modifying LibreNMS locations
if error := self.require_write_permission():
return error
action = request.POST.get("action")
pk = request.POST.get("pk")
if not pk:
messages.error(request, "No site ID provided.")
return redirect("plugins:netbox_librenms_plugin:site_location_sync")
site = self.get_site_by_pk(pk)
if not site:
messages.error(request, "Site not found.")
return redirect("plugins:netbox_librenms_plugin:site_location_sync")
if action == "update":
return self.update_librenms_location(request, site)
if action == "create":
return self.create_librenms_location(request, site)
messages.error(request, f"Unknown action '{action}'.")
return redirect("plugins:netbox_librenms_plugin:site_location_sync")
def get_site_by_pk(self, pk):
"""Return the Site for the given pk, or None if not found."""
try:
return Site.objects.get(pk=pk)
except ObjectDoesNotExist:
return None
def create_librenms_location(self, request, site):
"""Create a new location in LibreNMS from the given site."""
if site.latitude is None or site.longitude is None:
messages.warning(
request,
f"Latitude and/or longitude is missing. Cannot create location '{site.name}' in LibreNMS.",
)
return redirect("plugins:netbox_librenms_plugin:site_location_sync")
location_data = self.build_location_data(site)
success, message = self.librenms_api.add_location(location_data)
if success:
messages.success(request, f"Location '{site.name}' created in LibreNMS successfully.")
else:
messages.error(
request,
f"Failed to create location '{site.name}' in LibreNMS: {message}",
)
return redirect("plugins:netbox_librenms_plugin:site_location_sync")
def update_librenms_location(self, request, site):
"""Update an existing LibreNMS location with the site coordinates."""
if site.latitude is None or site.longitude is None:
messages.warning(
request,
f"Latitude and/or longitude is missing. Cannot update location '{site.name}' in LibreNMS.",
)
return redirect("plugins:netbox_librenms_plugin:site_location_sync")
success, librenms_locations = self.get_librenms_locations()
if not success:
messages.error(request, "Failed to retrieve LibreNMS locations.")
return redirect("plugins:netbox_librenms_plugin:site_location_sync")
matched_location = self.match_site_with_location(site, librenms_locations)
if not matched_location:
messages.error(request, f"Could not find matching location for site '{site.name}'")
return redirect("plugins:netbox_librenms_plugin:site_location_sync")
location_data = self.build_location_data(site, include_name=False)
success, message = self.librenms_api.update_location(matched_location["location"], location_data)
if success:
messages.success(request, f"Location '{site.name}' updated in LibreNMS successfully.")
else:
messages.error(
request,
f"Failed to update location '{site.name}' in LibreNMS: {message}",
)
return redirect("plugins:netbox_librenms_plugin:site_location_sync")
def build_location_data(self, site, include_name=True):
"""Build a location data dict from the given site."""
data = {"lat": str(site.latitude), "lng": str(site.longitude)}
if include_name:
data["location"] = site.name
return data