233 lines
8.9 KiB
Python
233 lines
8.9 KiB
Python
"""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)}"
|
|
)
|