first commit
This commit is contained in:
787
netbox_librenms_plugin/forms.py
Normal file
787
netbox_librenms_plugin/forms.py
Normal file
@@ -0,0 +1,787 @@
|
||||
# forms.py
|
||||
import logging
|
||||
|
||||
from dcim.choices import InterfaceTypeChoices
|
||||
from dcim.models import Device, DeviceRole, DeviceType, Location, Rack, Site
|
||||
from django import forms
|
||||
from django.db.models import Case, IntegerField, Value, When
|
||||
from django.http import QueryDict
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from netbox.forms import (
|
||||
NetBoxModelFilterSetForm,
|
||||
NetBoxModelForm,
|
||||
NetBoxModelImportForm,
|
||||
)
|
||||
from netbox.plugins import get_plugin_config
|
||||
from utilities.forms.fields import CSVChoiceField, DynamicModelMultipleChoiceField
|
||||
from virtualization.models import Cluster, VirtualMachine
|
||||
|
||||
from .models import InterfaceTypeMapping, LibreNMSSettings
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _get_librenms_server_choices():
|
||||
"""
|
||||
Helper function to get server choices from plugin configuration.
|
||||
Shared between ServerConfigForm and other forms that need server selection.
|
||||
"""
|
||||
choices = []
|
||||
|
||||
# Try to get multi-server configuration
|
||||
servers_config = get_plugin_config("netbox_librenms_plugin", "servers")
|
||||
|
||||
if servers_config and isinstance(servers_config, dict):
|
||||
# Multi-server configuration
|
||||
for key, config in servers_config.items():
|
||||
display_name = config.get("display_name", key)
|
||||
url = config.get("librenms_url", "Unknown URL")
|
||||
choices.append((key, f"{display_name} ({url})"))
|
||||
else:
|
||||
# Legacy single-server configuration
|
||||
legacy_url = get_plugin_config("netbox_librenms_plugin", "librenms_url")
|
||||
if legacy_url:
|
||||
choices.append(("default", f"Default Server ({legacy_url})"))
|
||||
else:
|
||||
choices.append(("default", "Default Server"))
|
||||
|
||||
return choices
|
||||
|
||||
|
||||
def _get_librenms_poller_group_choices():
|
||||
"""
|
||||
Helper function to get poller group choices from LibreNMS API.
|
||||
Shared between AddToLIbreSNMPV1V2 and AddToLIbreSNMPV3 forms.
|
||||
"""
|
||||
from .librenms_api import LibreNMSAPI
|
||||
|
||||
choices = [("0", "Default (0)")]
|
||||
|
||||
try:
|
||||
api = LibreNMSAPI()
|
||||
success, poller_groups = api.get_poller_groups()
|
||||
|
||||
if success:
|
||||
for group in poller_groups or []:
|
||||
group_id = str(group.get("id", ""))
|
||||
group_name = group.get("group_name", "")
|
||||
group_descr = group.get("descr", "")
|
||||
|
||||
if group_id:
|
||||
if group_descr and group_descr != group_name:
|
||||
label = f"{group_name} - {group_descr} ({group_id})"
|
||||
else:
|
||||
label = f"{group_name} ({group_id})"
|
||||
choices.append((group_id, label))
|
||||
except Exception:
|
||||
logger.exception("Failed to fetch LibreNMS poller groups; using default choices")
|
||||
|
||||
return choices
|
||||
|
||||
|
||||
class ServerConfigForm(NetBoxModelForm):
|
||||
"""
|
||||
Form for selecting the active LibreNMS server from configured servers.
|
||||
Handles server configuration changes only.
|
||||
"""
|
||||
|
||||
selected_server = forms.ChoiceField(
|
||||
label="LibreNMS Server",
|
||||
help_text="Select which LibreNMS server to use for synchronization operations",
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = LibreNMSSettings
|
||||
fields = ["selected_server"]
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.fields["selected_server"].choices = _get_librenms_server_choices()
|
||||
|
||||
|
||||
class ImportSettingsForm(NetBoxModelForm):
|
||||
"""
|
||||
Form for configuring device import settings including naming patterns
|
||||
and virtual chassis member naming.
|
||||
"""
|
||||
|
||||
vc_member_name_pattern = forms.CharField(
|
||||
label="Virtual Chassis Member Naming Pattern",
|
||||
max_length=100,
|
||||
required=False,
|
||||
strip=False, # Preserve leading/trailing whitespace
|
||||
widget=forms.TextInput(
|
||||
attrs={
|
||||
"placeholder": "-M{position}",
|
||||
}
|
||||
),
|
||||
)
|
||||
|
||||
use_sysname_default = forms.BooleanField(
|
||||
label="Use sysName",
|
||||
required=False,
|
||||
widget=forms.CheckboxInput(attrs={"class": "form-check-input"}),
|
||||
help_text="Use SNMP sysName instead of LibreNMS hostname when importing devices",
|
||||
)
|
||||
|
||||
strip_domain_default = forms.BooleanField(
|
||||
label="Strip domain",
|
||||
required=False,
|
||||
widget=forms.CheckboxInput(attrs={"class": "form-check-input"}),
|
||||
help_text="Remove domain suffix from device names during import",
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = LibreNMSSettings
|
||||
fields = [
|
||||
"vc_member_name_pattern",
|
||||
"use_sysname_default",
|
||||
"strip_domain_default",
|
||||
]
|
||||
|
||||
def clean_vc_member_name_pattern(self):
|
||||
"""
|
||||
Validate VC member name pattern for valid placeholders and formatting.
|
||||
|
||||
The pattern is used as a suffix appended to the master device name.
|
||||
Valid placeholders: {position}, {serial}
|
||||
At least one is required for uniqueness.
|
||||
"""
|
||||
pattern = self.cleaned_data.get("vc_member_name_pattern")
|
||||
|
||||
if not pattern:
|
||||
return pattern
|
||||
|
||||
# Check for valid placeholder names using regex
|
||||
import re
|
||||
|
||||
valid_placeholders = {"position", "serial"}
|
||||
found_placeholders = set(re.findall(r"\{(\w+)\}", pattern))
|
||||
invalid_placeholders = found_placeholders - valid_placeholders
|
||||
|
||||
if invalid_placeholders:
|
||||
invalid_list = ", ".join(f"{{{p}}}" for p in sorted(invalid_placeholders))
|
||||
error_msg = f"Invalid placeholder(s): {invalid_list}. Valid options are: {{position}}, {{serial}}"
|
||||
raise forms.ValidationError(error_msg)
|
||||
|
||||
# Check required: must have at least one unique identifier
|
||||
if "{position}" not in pattern and "{serial}" not in pattern:
|
||||
raise forms.ValidationError(
|
||||
"The naming pattern must include either {{position}} or {{serial}} "
|
||||
"placeholder to ensure unique member names."
|
||||
)
|
||||
|
||||
# Test the pattern can be formatted without errors
|
||||
test_vars = {
|
||||
"position": 1,
|
||||
"serial": "ABC123",
|
||||
}
|
||||
|
||||
try:
|
||||
test_result = pattern.format(**test_vars)
|
||||
|
||||
# Check result isn't empty or just whitespace
|
||||
if not test_result.strip():
|
||||
raise forms.ValidationError(
|
||||
"The pattern results in an empty suffix. Please include some text content in the pattern."
|
||||
)
|
||||
|
||||
except KeyError as e:
|
||||
# This should be caught by check above, but just in case
|
||||
raise forms.ValidationError(
|
||||
f"Invalid placeholder in pattern: {e}. Valid options are: {{position}}, {{serial}}"
|
||||
)
|
||||
except (ValueError, IndexError) as e:
|
||||
raise forms.ValidationError(f"Invalid pattern syntax: {str(e)}")
|
||||
|
||||
return pattern
|
||||
|
||||
|
||||
# Keep for backward compatibility if needed elsewhere
|
||||
class LibreNMSSettingsForm(ServerConfigForm):
|
||||
"""
|
||||
Deprecated: Use ServerConfigForm or ImportSettingsForm instead.
|
||||
Kept for backward compatibility.
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class InterfaceTypeMappingForm(NetBoxModelForm):
|
||||
"""
|
||||
Form for creating and editing interface type mappings between LibreNMS and NetBox.
|
||||
Allows mapping of LibreNMS interface types and speeds to NetBox interface types.
|
||||
"""
|
||||
|
||||
class Meta:
|
||||
model = InterfaceTypeMapping
|
||||
fields = ["librenms_type", "librenms_speed", "netbox_type", "description"]
|
||||
|
||||
|
||||
class InterfaceTypeMappingImportForm(NetBoxModelImportForm):
|
||||
"""
|
||||
Form for bulk importing interface type mappings from CSV/JSON/YAML.
|
||||
Supports importing LibreNMS interface type and speed mappings to NetBox interface types.
|
||||
"""
|
||||
|
||||
netbox_type = CSVChoiceField(
|
||||
label=_("NetBox Type"),
|
||||
choices=InterfaceTypeChoices,
|
||||
help_text=_("NetBox interface type"),
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = InterfaceTypeMapping
|
||||
fields = ["librenms_type", "librenms_speed", "netbox_type", "description"]
|
||||
|
||||
|
||||
class InterfaceTypeMappingFilterForm(NetBoxModelFilterSetForm):
|
||||
"""
|
||||
Form for filtering interface type mappings based on LibreNMS and NetBox attributes.
|
||||
Provides filtering options for LibreNMS type, speed, and NetBox type.
|
||||
"""
|
||||
|
||||
librenms_type = forms.CharField(required=False, label="LibreNMS Type")
|
||||
librenms_speed = forms.IntegerField(
|
||||
required=False,
|
||||
label="LibreNMS Speed (Kbps)",
|
||||
help_text="Filter by interface speed in Kbps",
|
||||
)
|
||||
netbox_type = forms.ChoiceField(
|
||||
required=False,
|
||||
label="NetBox Type",
|
||||
choices=[("", "---------")] + list(InterfaceTypeChoices),
|
||||
)
|
||||
description = forms.CharField(
|
||||
required=False,
|
||||
label="Description",
|
||||
help_text="Filter by description (partial match)",
|
||||
)
|
||||
|
||||
model = InterfaceTypeMapping
|
||||
|
||||
|
||||
class AddToLIbreSNMPV1V2(forms.Form):
|
||||
"""
|
||||
Form for adding devices to LibreNMS using SNMPv1 or SNMPv2c authentication.
|
||||
Collects hostname/IP and SNMP community string information.
|
||||
The SNMP version (v1 or v2c) is selected via a toggle button in the template.
|
||||
"""
|
||||
|
||||
hostname = forms.CharField(
|
||||
label="Hostname/IP",
|
||||
max_length=255,
|
||||
required=True,
|
||||
)
|
||||
community = forms.CharField(label="SNMP Community", max_length=255, required=True)
|
||||
port = forms.IntegerField(
|
||||
label="SNMP Port",
|
||||
required=False,
|
||||
help_text="Leave blank to use default SNMP port (161)",
|
||||
widget=forms.NumberInput(attrs={"placeholder": "161"}),
|
||||
)
|
||||
transport = forms.ChoiceField(
|
||||
label="Transport",
|
||||
choices=[
|
||||
("udp", "UDP"),
|
||||
("tcp", "TCP"),
|
||||
("udp6", "UDP6"),
|
||||
("tcp6", "TCP6"),
|
||||
],
|
||||
required=False,
|
||||
initial="udp",
|
||||
)
|
||||
port_association_mode = forms.ChoiceField(
|
||||
label="Port Association Mode",
|
||||
choices=[
|
||||
("ifIndex", "ifIndex"),
|
||||
("ifName", "ifName"),
|
||||
("ifDescr", "ifDescr"),
|
||||
("ifAlias", "ifAlias"),
|
||||
],
|
||||
required=False,
|
||||
initial="ifIndex",
|
||||
help_text="Method to identify ports",
|
||||
)
|
||||
poller_group = forms.ChoiceField(
|
||||
label="Poller Group",
|
||||
required=False,
|
||||
help_text="Poller group for distributed poller setup",
|
||||
)
|
||||
force_add = forms.BooleanField(
|
||||
label="Force Add",
|
||||
required=False,
|
||||
initial=False,
|
||||
help_text="Skip duplicate device and SNMP reachability checks (hostname must still be unique)",
|
||||
)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.fields["poller_group"].choices = _get_librenms_poller_group_choices()
|
||||
|
||||
|
||||
class AddToLIbreSNMPV3(forms.Form):
|
||||
"""
|
||||
Form for adding devices to LibreNMS using SNMPv3 authentication.
|
||||
Provides comprehensive SNMPv3 configuration options including authentication and encryption settings.
|
||||
"""
|
||||
|
||||
hostname = forms.CharField(
|
||||
label="Hostname/IP",
|
||||
max_length=255,
|
||||
required=True,
|
||||
)
|
||||
snmp_version = forms.CharField(widget=forms.HiddenInput(), initial="v3")
|
||||
authlevel = forms.ChoiceField(
|
||||
label="Auth Level",
|
||||
choices=[
|
||||
("noAuthNoPriv", "noAuthNoPriv"),
|
||||
("authNoPriv", "authNoPriv"),
|
||||
("authPriv", "authPriv"),
|
||||
],
|
||||
required=True,
|
||||
)
|
||||
authname = forms.CharField(label="Auth Username", max_length=255, required=True)
|
||||
authpass = forms.CharField(
|
||||
label="Auth Password",
|
||||
max_length=255,
|
||||
required=True,
|
||||
widget=forms.PasswordInput(render_value=True),
|
||||
)
|
||||
authalgo = forms.ChoiceField(
|
||||
label="Auth Algorithm",
|
||||
choices=[
|
||||
("SHA", "SHA"),
|
||||
("MD5", "MD5"),
|
||||
("SHA-224", "SHA-224"),
|
||||
("SHA-256", "SHA-256"),
|
||||
("SHA-384", "SHA-384"),
|
||||
("SHA-512", "SHA-512"),
|
||||
],
|
||||
required=True,
|
||||
)
|
||||
cryptopass = forms.CharField(
|
||||
label="Crypto Password",
|
||||
max_length=255,
|
||||
required=True,
|
||||
widget=forms.PasswordInput(render_value=True),
|
||||
)
|
||||
cryptoalgo = forms.ChoiceField(
|
||||
label="Crypto Algorithm",
|
||||
choices=[("AES", "AES"), ("DES", "DES")],
|
||||
required=True,
|
||||
)
|
||||
port = forms.IntegerField(
|
||||
label="SNMP Port",
|
||||
required=False,
|
||||
help_text="Leave blank to use default SNMP port (161)",
|
||||
widget=forms.NumberInput(attrs={"placeholder": "161"}),
|
||||
)
|
||||
transport = forms.ChoiceField(
|
||||
label="Transport",
|
||||
choices=[
|
||||
("udp", "UDP"),
|
||||
("tcp", "TCP"),
|
||||
("udp6", "UDP6"),
|
||||
("tcp6", "TCP6"),
|
||||
],
|
||||
required=False,
|
||||
initial="udp",
|
||||
)
|
||||
port_association_mode = forms.ChoiceField(
|
||||
label="Port Association Mode",
|
||||
choices=[
|
||||
("ifIndex", "ifIndex"),
|
||||
("ifName", "ifName"),
|
||||
("ifDescr", "ifDescr"),
|
||||
("ifAlias", "ifAlias"),
|
||||
],
|
||||
required=False,
|
||||
initial="ifIndex",
|
||||
help_text="Method to identify ports",
|
||||
)
|
||||
poller_group = forms.ChoiceField(
|
||||
label="Poller Group",
|
||||
required=False,
|
||||
help_text="Poller group for distributed poller setup",
|
||||
)
|
||||
force_add = forms.BooleanField(
|
||||
label="Force Add",
|
||||
required=False,
|
||||
initial=False,
|
||||
help_text="Skip duplicate device and SNMP reachability checks (hostname must still be unique)",
|
||||
)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.fields["poller_group"].choices = _get_librenms_poller_group_choices()
|
||||
|
||||
|
||||
class DeviceStatusFilterForm(NetBoxModelFilterSetForm):
|
||||
"""
|
||||
Filter form for Device Status view - shows NetBox devices and their LibreNMS status.
|
||||
"""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
# Remove the saved filter field if it exists
|
||||
if "filter_id" in self.fields:
|
||||
del self.fields["filter_id"]
|
||||
|
||||
site = DynamicModelMultipleChoiceField(queryset=Site.objects.all(), required=False)
|
||||
location = DynamicModelMultipleChoiceField(queryset=Location.objects.all(), required=False)
|
||||
rack = DynamicModelMultipleChoiceField(queryset=Rack.objects.all(), required=False)
|
||||
device_type = DynamicModelMultipleChoiceField(queryset=DeviceType.objects.all(), required=False)
|
||||
role = DynamicModelMultipleChoiceField(queryset=DeviceRole.objects.all(), required=False)
|
||||
|
||||
model = Device
|
||||
|
||||
|
||||
class LibreNMSImportFilterForm(forms.Form):
|
||||
"""
|
||||
Filter form for LibreNMS Import view - shows LibreNMS devices for import.
|
||||
Uses a simple Django form instead of NetBox model forms.
|
||||
"""
|
||||
|
||||
# LibreNMS filters
|
||||
librenms_location = forms.ChoiceField(
|
||||
required=False,
|
||||
label="LibreNMS Location",
|
||||
choices=[("", "All Locations")], # Default, will be populated in __init__
|
||||
widget=forms.Select(attrs={"class": "form-select"}),
|
||||
)
|
||||
librenms_type = forms.ChoiceField(
|
||||
required=False,
|
||||
label="LibreNMS 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"),
|
||||
],
|
||||
)
|
||||
librenms_os = forms.CharField(
|
||||
required=False,
|
||||
label="Operating System",
|
||||
widget=forms.TextInput(attrs={"placeholder": "e.g., ios, linux, junos"}),
|
||||
)
|
||||
librenms_hostname = forms.CharField(
|
||||
required=False,
|
||||
label="LibreNMS Hostname",
|
||||
widget=forms.TextInput(attrs={"placeholder": "Partial hostname match"}),
|
||||
help_text="IP address or FQDN used to add device to LibreNMS",
|
||||
)
|
||||
librenms_sysname = forms.CharField(
|
||||
required=False,
|
||||
label="LibreNMS System Name",
|
||||
widget=forms.TextInput(attrs={"placeholder": "Exact or partial sysName match"}),
|
||||
help_text="SNMP sysName. (exact match only; combine with another filter for partial matching)",
|
||||
)
|
||||
librenms_hardware = forms.CharField(
|
||||
required=False,
|
||||
label="Hardware",
|
||||
widget=forms.TextInput(attrs={"placeholder": "e.g., C9300-48P, ASR-920"}),
|
||||
help_text="LibreNMS hardware model (partial match)",
|
||||
)
|
||||
show_disabled = forms.BooleanField(
|
||||
required=False,
|
||||
initial=False,
|
||||
label="Include Disabled Devices",
|
||||
widget=forms.CheckboxInput(attrs={"class": "form-check-input"}),
|
||||
)
|
||||
enable_vc_detection = forms.BooleanField(
|
||||
required=False,
|
||||
initial=False,
|
||||
label="Include Virtual Chassis Detection",
|
||||
help_text="Run additional stack checks during the search. Will increase processing time.",
|
||||
widget=forms.CheckboxInput(attrs={"class": "form-check-input"}),
|
||||
)
|
||||
clear_cache = forms.BooleanField(
|
||||
required=False,
|
||||
initial=False,
|
||||
label="Clear cache before search",
|
||||
help_text="Discard the cache and pull fresh data from both LibreNMS and NetBox.",
|
||||
widget=forms.CheckboxInput(attrs={"class": "form-check-input"}),
|
||||
)
|
||||
exclude_existing = forms.BooleanField(
|
||||
required=False,
|
||||
initial=False,
|
||||
label="Exclude Existing Devices",
|
||||
help_text="Hide devices that already exist in NetBox",
|
||||
widget=forms.CheckboxInput(attrs={"class": "form-check-input"}),
|
||||
)
|
||||
use_background_job = forms.BooleanField(
|
||||
required=False,
|
||||
initial=True,
|
||||
label="Run as background job",
|
||||
help_text="Recommended: Jobs are logged and can be cancelled.",
|
||||
widget=forms.CheckboxInput(attrs={"class": "form-check-input"}),
|
||||
)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""Initialize the form and populate dynamic choices."""
|
||||
# For bound forms, ensure use_background_job defaults to 'on' if not present
|
||||
# This handles the case where checkbox is checked by default but not in GET params
|
||||
# Only apply this default when no filters are applied (initial page load)
|
||||
if args and isinstance(args[0], (dict, QueryDict)):
|
||||
# Form is being bound with data (GET/POST dict or QueryDict)
|
||||
data = args[0].copy() if hasattr(args[0], "copy") else dict(args[0])
|
||||
# If use_background_job is not in the data, add it with default 'on'
|
||||
# This makes the checkbox checked by default even on first submission
|
||||
# Only do this if no filter fields are set (initial page load scenario)
|
||||
filter_fields = [
|
||||
"librenms_location",
|
||||
"librenms_type",
|
||||
"librenms_os",
|
||||
"librenms_hostname",
|
||||
"librenms_sysname",
|
||||
"librenms_hardware",
|
||||
]
|
||||
has_filters = any(data.get(field) for field in filter_fields)
|
||||
|
||||
non_option_fields = [
|
||||
f for f in filter_fields if data.get(f) not in (None, "", []) and str(data.get(f, "")).strip()
|
||||
]
|
||||
has_option_only = bool(data) and not bool(non_option_fields) and not has_filters
|
||||
|
||||
# Apply default only on initial load (no filters, no job_id, no real submission)
|
||||
if "use_background_job" not in data and not data.get("job_id") and not has_filters and not has_option_only:
|
||||
data["use_background_job"] = "on"
|
||||
args = (data,) + args[1:]
|
||||
|
||||
super().__init__(*args, **kwargs)
|
||||
# Populate LibreNMS location choices dynamically
|
||||
self._populate_librenms_locations()
|
||||
|
||||
def clean(self):
|
||||
cleaned_data = super().clean()
|
||||
|
||||
# Only enforce filter requirement when the user explicitly submits the form
|
||||
if self.data.get("apply_filters"):
|
||||
filter_fields = (
|
||||
"librenms_location",
|
||||
"librenms_type",
|
||||
"librenms_os",
|
||||
"librenms_hostname",
|
||||
"librenms_sysname",
|
||||
"librenms_hardware",
|
||||
)
|
||||
|
||||
if not any(cleaned_data.get(field) for field in filter_fields):
|
||||
raise forms.ValidationError("Please select at least one LibreNMS filter before applying the search.")
|
||||
|
||||
return cleaned_data
|
||||
|
||||
def _populate_librenms_locations(self):
|
||||
"""Fetch and populate LibreNMS locations in the dropdown."""
|
||||
from django.core.cache import cache
|
||||
|
||||
from netbox_librenms_plugin.import_utils.cache import get_location_choices_cache_key
|
||||
from netbox_librenms_plugin.librenms_api import LibreNMSAPI
|
||||
|
||||
try:
|
||||
# Determine server_key cheaply from settings to check cache before instantiating the API
|
||||
try:
|
||||
from netbox_librenms_plugin.models import LibreNMSSettings
|
||||
|
||||
_settings = LibreNMSSettings.objects.first()
|
||||
_server_key = (_settings.selected_server if _settings else None) or "default"
|
||||
except Exception:
|
||||
_server_key = "default"
|
||||
|
||||
cache_key = get_location_choices_cache_key(_server_key)
|
||||
cached_choices = cache.get(cache_key)
|
||||
if cached_choices is not None:
|
||||
self.fields["librenms_location"].choices = cached_choices
|
||||
return
|
||||
|
||||
# Cache miss — instantiate the API client and fetch
|
||||
api = LibreNMSAPI()
|
||||
# Recompute cache_key with the resolved server_key in case it differs from settings
|
||||
cache_key = get_location_choices_cache_key(api.server_key)
|
||||
# Second cache check: the resolved server_key may differ from the settings key
|
||||
cached_choices = cache.get(cache_key)
|
||||
if cached_choices is not None:
|
||||
self.fields["librenms_location"].choices = cached_choices
|
||||
return
|
||||
|
||||
# Fetch locations from LibreNMS
|
||||
success, locations = api.get_locations()
|
||||
|
||||
if success and locations:
|
||||
# Build choices list: (id, name)
|
||||
choices = [("", "All Locations")]
|
||||
for loc in locations:
|
||||
loc_id = str(loc.get("id", ""))
|
||||
loc_name = loc.get("location", f"Location {loc_id}")
|
||||
choices.append((loc_id, loc_name))
|
||||
|
||||
# Sort by name
|
||||
choices[1:] = sorted(choices[1:], key=lambda x: x[1])
|
||||
|
||||
self.fields["librenms_location"].choices = choices
|
||||
|
||||
# Cache using configured timeout (default 300s)
|
||||
cache.set(cache_key, choices, timeout=api.cache_timeout)
|
||||
logger.info(f"Loaded {len(choices) - 1} LibreNMS locations")
|
||||
else:
|
||||
logger.warning(f"Failed to load LibreNMS locations: {locations}")
|
||||
except Exception as e:
|
||||
logger.exception(f"Error loading LibreNMS locations: {e}")
|
||||
# Keep default choices on error
|
||||
|
||||
|
||||
class VirtualMachineStatusFilterForm(NetBoxModelFilterSetForm):
|
||||
"""
|
||||
Form for filtering virtual machine status information in NetBox.
|
||||
"""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""Initialize the form and remove the filter_id field if it exists."""
|
||||
super().__init__(*args, **kwargs)
|
||||
# Remove the saved filter field if it exists
|
||||
if "filter_id" in self.fields:
|
||||
del self.fields["filter_id"]
|
||||
|
||||
virtualmachine = DynamicModelMultipleChoiceField(queryset=VirtualMachine.objects.all(), required=False)
|
||||
site = DynamicModelMultipleChoiceField(queryset=Site.objects.all(), required=False)
|
||||
cluster = DynamicModelMultipleChoiceField(queryset=Cluster.objects.all(), required=False)
|
||||
|
||||
model = VirtualMachine
|
||||
|
||||
|
||||
class DeviceImportConfigForm(forms.Form):
|
||||
"""
|
||||
Form for configuring import of LibreNMS devices with missing prerequisites.
|
||||
Allows user to manually map LibreNMS device data to NetBox objects.
|
||||
"""
|
||||
|
||||
device_id = forms.IntegerField(widget=forms.HiddenInput(), required=True)
|
||||
hostname = forms.CharField(disabled=True, required=False, label="Device Hostname")
|
||||
hardware = forms.CharField(disabled=True, required=False, label="Hardware")
|
||||
librenms_location = forms.CharField(disabled=True, required=False, label="LibreNMS Location")
|
||||
|
||||
# Required mappings
|
||||
site = forms.ModelChoiceField(
|
||||
queryset=Site.objects.all(),
|
||||
required=True,
|
||||
label="NetBox Site",
|
||||
help_text="Select the NetBox site for this device",
|
||||
widget=forms.Select(attrs={"class": "form-select"}),
|
||||
)
|
||||
device_type = forms.ModelChoiceField(
|
||||
queryset=DeviceType.objects.all(),
|
||||
required=True,
|
||||
label="Device Type",
|
||||
help_text="Select the NetBox device type",
|
||||
widget=forms.Select(attrs={"class": "form-select"}),
|
||||
)
|
||||
device_role = forms.ModelChoiceField(
|
||||
queryset=DeviceRole.objects.all(),
|
||||
required=True,
|
||||
label="Device Role",
|
||||
help_text="Select the device role",
|
||||
widget=forms.Select(attrs={"class": "form-select"}),
|
||||
)
|
||||
|
||||
# Optional mappings
|
||||
platform = forms.ModelChoiceField(
|
||||
queryset=None,
|
||||
required=False,
|
||||
label="Platform",
|
||||
help_text="Select platform (optional)",
|
||||
widget=forms.Select(attrs={"class": "form-select"}),
|
||||
)
|
||||
|
||||
# Sync options
|
||||
sync_interfaces = forms.BooleanField(
|
||||
initial=True,
|
||||
required=False,
|
||||
label="Sync Interfaces",
|
||||
help_text="Automatically sync interfaces from LibreNMS after import",
|
||||
)
|
||||
sync_cables = forms.BooleanField(
|
||||
initial=True,
|
||||
required=False,
|
||||
label="Sync Cables",
|
||||
help_text="Automatically sync cable connections from LibreNMS after import",
|
||||
)
|
||||
sync_ips = forms.BooleanField(
|
||||
initial=True,
|
||||
required=False,
|
||||
label="Sync IP Addresses",
|
||||
help_text="Automatically sync IP addresses from LibreNMS after import",
|
||||
)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""
|
||||
Initialize form with LibreNMS device data and validation results.
|
||||
|
||||
Accepts additional kwargs:
|
||||
- libre_device: LibreNMS device dictionary
|
||||
- validation: Validation result dictionary
|
||||
- suggested_site: Pre-selected site
|
||||
- suggested_device_type: Pre-selected device type
|
||||
- suggested_role: Pre-selected device role
|
||||
"""
|
||||
# Extract custom kwargs
|
||||
libre_device = kwargs.pop("libre_device", {})
|
||||
validation = kwargs.pop("validation", {})
|
||||
suggested_site = kwargs.pop("suggested_site", None)
|
||||
suggested_device_type = kwargs.pop("suggested_device_type", None)
|
||||
suggested_role = kwargs.pop("suggested_role", None)
|
||||
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
# Import Platform here to avoid circular imports
|
||||
from dcim.models import Platform
|
||||
|
||||
self.fields["platform"].queryset = Platform.objects.all()
|
||||
|
||||
# Set initial values from LibreNMS device
|
||||
if libre_device:
|
||||
self.fields["device_id"].initial = libre_device.get("device_id")
|
||||
self.fields["hostname"].initial = libre_device.get("hostname", "")
|
||||
self.fields["hardware"].initial = libre_device.get("hardware", "")
|
||||
self.fields["librenms_location"].initial = libre_device.get("location", "")
|
||||
|
||||
# Set suggested values from validation
|
||||
if suggested_site:
|
||||
self.fields["site"].initial = suggested_site
|
||||
elif validation and validation.get("site", {}).get("site"):
|
||||
self.fields["site"].initial = validation["site"]["site"]
|
||||
|
||||
if suggested_device_type:
|
||||
self.fields["device_type"].initial = suggested_device_type
|
||||
elif validation and validation.get("device_type", {}).get("device_type"):
|
||||
self.fields["device_type"].initial = validation["device_type"]["device_type"]
|
||||
|
||||
if suggested_role:
|
||||
self.fields["device_role"].initial = suggested_role
|
||||
elif validation and validation.get("device_role", {}).get("role"):
|
||||
self.fields["device_role"].initial = validation["device_role"]["role"]
|
||||
|
||||
if validation and validation.get("platform", {}).get("platform"):
|
||||
self.fields["platform"].initial = validation["platform"]["platform"]
|
||||
|
||||
# Filter device types by suggestions if available
|
||||
if validation and validation.get("device_type", {}).get("suggestions"):
|
||||
suggestions = validation["device_type"]["suggestions"]
|
||||
if suggestions:
|
||||
# Annotate with suggested_order so suggested types sort first
|
||||
suggested_ids = [s["device_type"].id for s in suggestions]
|
||||
priority = Case(
|
||||
*[When(id=pk, then=Value(i)) for i, pk in enumerate(suggested_ids)],
|
||||
default=Value(len(suggested_ids)),
|
||||
output_field=IntegerField(),
|
||||
)
|
||||
self.fields["device_type"].queryset = DeviceType.objects.annotate(suggested_order=priority).order_by(
|
||||
"suggested_order", "manufacturer__name", "model"
|
||||
)
|
||||
Reference in New Issue
Block a user