first commit
This commit is contained in:
232
netbox_librenms_plugin/import_utils/cache.py
Normal file
232
netbox_librenms_plugin/import_utils/cache.py
Normal file
@@ -0,0 +1,232 @@
|
||||
"""Cache key generation and management for device import operations."""
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
import logging
|
||||
|
||||
from django.core.cache import cache
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _build_filter_hash(filters: dict) -> str:
|
||||
"""
|
||||
Build a stable, collision-free hash from a filter dict.
|
||||
|
||||
Removes None values (preserves valid falsy values like 0 and False),
|
||||
sorts by key, and returns the first 16 hex characters of the SHA-256
|
||||
digest of the JSON-serialized result.
|
||||
"""
|
||||
return hashlib.sha256(
|
||||
json.dumps({k: v for k, v in filters.items() if v is not None}, sort_keys=True, separators=(",", ":")).encode()
|
||||
).hexdigest()[:16]
|
||||
|
||||
|
||||
def get_location_choices_cache_key(server_key: str) -> str:
|
||||
"""Return the cache key for LibreNMS location choices for a given server."""
|
||||
return f"librenms_locations_choices:{server_key}"
|
||||
|
||||
|
||||
def get_cache_metadata_key(
|
||||
server_key: str, filters: dict, vc_enabled: bool, use_sysname: bool = True, strip_domain: bool = False
|
||||
) -> str:
|
||||
"""
|
||||
Generate a consistent cache metadata key from filter parameters.
|
||||
|
||||
Args:
|
||||
server_key: LibreNMS server identifier
|
||||
filters: Filter dictionary
|
||||
vc_enabled: Whether VC detection is enabled
|
||||
use_sysname: Whether sysName is preferred over hostname for device naming
|
||||
strip_domain: Whether domain suffix is stripped from device names
|
||||
|
||||
Returns:
|
||||
str: Consistent cache key for metadata
|
||||
"""
|
||||
# Sort filter items to ensure consistent key generation; use "is not None" to preserve
|
||||
# valid falsy values like 0 and False (filtering only None/missing entries).
|
||||
# Use JSON serialization for a stable, collision-free hash (avoids issues with
|
||||
# values containing "=" or "_" that could collide with the key separators).
|
||||
filter_hash = _build_filter_hash(filters)
|
||||
return f"librenms_filter_cache_metadata_{server_key}_{filter_hash}_{vc_enabled}_sysname={use_sysname}_strip={strip_domain}"
|
||||
|
||||
|
||||
def get_active_cached_searches(server_key: str) -> list[dict]:
|
||||
"""
|
||||
Retrieve all active cached searches for a server and enrich with display-friendly values.
|
||||
|
||||
Enriches raw filter IDs with human-readable names by looking up location names
|
||||
from cached choices and converting type codes to display names.
|
||||
|
||||
Args:
|
||||
server_key: LibreNMS server identifier
|
||||
|
||||
Returns:
|
||||
List of dicts containing cache metadata with enriched display_filters
|
||||
"""
|
||||
from datetime import datetime, timezone
|
||||
|
||||
cache_index_key = f"librenms_cache_index_{server_key}"
|
||||
cache_index = cache.get(cache_index_key, [])
|
||||
|
||||
active_searches = []
|
||||
valid_cache_keys = []
|
||||
|
||||
# Get location and type choices for enriching display
|
||||
location_choices = {}
|
||||
type_choices = {
|
||||
"": "All Types",
|
||||
"network": "Network",
|
||||
"server": "Server",
|
||||
"storage": "Storage",
|
||||
"wireless": "Wireless",
|
||||
"firewall": "Firewall",
|
||||
"power": "Power",
|
||||
"appliance": "Appliance",
|
||||
"printer": "Printer",
|
||||
"loadbalancer": "Load Balancer",
|
||||
"other": "Other",
|
||||
}
|
||||
|
||||
# Get cached location choices for enrichment; scoped by server_key so labels
|
||||
# from different LibreNMS servers don't bleed into each other's filter summaries.
|
||||
location_cache_key = get_location_choices_cache_key(server_key)
|
||||
cached_locations = cache.get(location_cache_key)
|
||||
if cached_locations:
|
||||
location_choices = dict(cached_locations)
|
||||
|
||||
for cache_key in cache_index:
|
||||
metadata = cache.get(cache_key)
|
||||
if metadata:
|
||||
# Cache still exists, calculate time remaining
|
||||
cache_timeout = metadata.get("cache_timeout", 300)
|
||||
now = datetime.now(timezone.utc)
|
||||
try:
|
||||
cached_at_raw = metadata.get("cached_at")
|
||||
if isinstance(cached_at_raw, datetime):
|
||||
cached_at = cached_at_raw
|
||||
elif cached_at_raw:
|
||||
cached_at = datetime.fromisoformat(cached_at_raw)
|
||||
else:
|
||||
cached_at = datetime.fromtimestamp(0, timezone.utc)
|
||||
# Normalize naive datetimes (e.g., stored without tzinfo) to UTC
|
||||
if cached_at.tzinfo is None:
|
||||
cached_at = cached_at.replace(tzinfo=timezone.utc)
|
||||
except (ValueError, TypeError):
|
||||
cached_at = datetime.fromtimestamp(0, timezone.utc)
|
||||
age_seconds = (now - cached_at).total_seconds()
|
||||
remaining_seconds = max(0, cache_timeout - age_seconds)
|
||||
|
||||
if remaining_seconds > 0:
|
||||
# Add remaining time and cache key
|
||||
metadata["remaining_seconds"] = int(remaining_seconds)
|
||||
metadata["cache_key"] = cache_key
|
||||
# Store numeric sort key so the final sort is unambiguous
|
||||
metadata["cached_at_ts"] = cached_at.timestamp()
|
||||
|
||||
# Enrich filters with human-readable display values
|
||||
if "filters" in metadata:
|
||||
display_filters = metadata["filters"].copy()
|
||||
# Convert location ID to location name
|
||||
if "location" in display_filters and display_filters["location"] in location_choices:
|
||||
display_filters["location"] = location_choices[display_filters["location"]]
|
||||
# Convert type code to display name
|
||||
if "type" in display_filters and display_filters["type"] in type_choices:
|
||||
display_filters["type"] = type_choices[display_filters["type"]]
|
||||
metadata["display_filters"] = display_filters
|
||||
else:
|
||||
# Fallback if filters key missing
|
||||
metadata["display_filters"] = {}
|
||||
|
||||
active_searches.append(metadata)
|
||||
valid_cache_keys.append(cache_key)
|
||||
|
||||
# Clean up index if any keys have expired
|
||||
if len(valid_cache_keys) < len(cache_index):
|
||||
cache.set(cache_index_key, valid_cache_keys, timeout=3600)
|
||||
|
||||
# Sort by most recent first
|
||||
active_searches.sort(key=lambda x: x.get("cached_at_ts", 0.0), reverse=True)
|
||||
|
||||
return active_searches
|
||||
|
||||
|
||||
def get_validated_device_cache_key(
|
||||
server_key: str,
|
||||
filters: dict,
|
||||
device_id: int | str,
|
||||
vc_enabled: bool,
|
||||
use_sysname: bool = True,
|
||||
strip_domain: bool = False,
|
||||
) -> str:
|
||||
"""
|
||||
Generate a consistent cache key for validated device data.
|
||||
|
||||
This ensures both synchronous and background job processing use the same
|
||||
cache keys, avoiding duplicate validation work and cache entries.
|
||||
|
||||
Args:
|
||||
server_key: LibreNMS server key
|
||||
filters: Filter dict with location, type, os, hostname, sysname, hardware keys
|
||||
device_id: LibreNMS device ID
|
||||
vc_enabled: Whether virtual chassis detection was enabled
|
||||
use_sysname: Whether sysName is preferred over hostname for device naming
|
||||
strip_domain: Whether domain suffix is stripped from device names
|
||||
|
||||
Returns:
|
||||
str: Cache key for the validated device
|
||||
|
||||
Example:
|
||||
>>> key = get_validated_device_cache_key('default', {'location': 'NYC'}, 123, True)
|
||||
>>> key
|
||||
'validated_device_default_e3b0c44298fc1c14_123_vc'
|
||||
"""
|
||||
# Sort filters for a deterministic, cross-process stable hash; None values are excluded
|
||||
# (consistent with get_cache_metadata_key).
|
||||
filter_hash = _build_filter_hash(filters)
|
||||
vc_part = "vc" if vc_enabled else "novc"
|
||||
return (
|
||||
f"validated_device_{server_key}_{filter_hash}_{device_id}_{vc_part}_sysname={use_sysname}_strip={strip_domain}"
|
||||
)
|
||||
|
||||
|
||||
def get_import_device_cache_key(device_id: int | str, server_key: str = "default") -> str:
|
||||
"""
|
||||
Generate cache key for raw LibreNMS device data.
|
||||
|
||||
This key is used to cache raw device data (without validation metadata)
|
||||
to avoid redundant API calls when users interact with dropdowns during
|
||||
the import workflow.
|
||||
|
||||
Args:
|
||||
device_id: LibreNMS device ID
|
||||
server_key: LibreNMS server identifier for multi-server setups. Defaults to "default" for backward compatibility.
|
||||
|
||||
Returns:
|
||||
str: Cache key for the device data
|
||||
|
||||
Example:
|
||||
>>> get_import_device_cache_key(123, "production")
|
||||
'import_device_data_production_123'
|
||||
"""
|
||||
return f"import_device_data_{server_key}_{device_id}"
|
||||
|
||||
|
||||
def get_import_search_cache_key(server_key: str, api_filters: dict, client_filters: dict) -> str:
|
||||
"""
|
||||
Generate a deterministic cache key for a LibreNMS device search result.
|
||||
|
||||
The key encodes the server, API-side filters, and client-side filters so
|
||||
that different filter combinations produce distinct cache entries.
|
||||
|
||||
Args:
|
||||
server_key: Resolved LibreNMS server key (use ``api.server_key``).
|
||||
api_filters: Filters forwarded to the LibreNMS API.
|
||||
client_filters: Filters applied client-side after the API response.
|
||||
|
||||
Returns:
|
||||
str: Cache key for the import search result.
|
||||
"""
|
||||
return (
|
||||
f"librenms_devices_import_{server_key}_{_build_filter_hash(api_filters)}_{_build_filter_hash(client_filters)}"
|
||||
)
|
||||
Reference in New Issue
Block a user