diff --git a/netbox/core/exceptions.py b/netbox/core/exceptions.py
index 8412b0378..5790704c2 100644
--- a/netbox/core/exceptions.py
+++ b/netbox/core/exceptions.py
@@ -1,2 +1,9 @@
+from django.core.exceptions import ImproperlyConfigured
+
+
class SyncError(Exception):
pass
+
+
+class IncompatiblePluginError(ImproperlyConfigured):
+ pass
diff --git a/netbox/core/plugins.py b/netbox/core/plugins.py
index d31a699e4..e4d14b810 100644
--- a/netbox/core/plugins.py
+++ b/netbox/core/plugins.py
@@ -65,9 +65,11 @@ class Plugin:
is_certified: bool = False
release_latest: PluginVersion = field(default_factory=PluginVersion)
release_recent_history: list[PluginVersion] = field(default_factory=list)
- is_local: bool = False # extra field for locally installed plugins
- is_installed: bool = False
+ is_local: bool = False # Indicates that the plugin is listed in settings.PLUGINS (i.e. installed)
+ is_loaded: bool = False # Indicates whether the plugin successfully loaded at launch
installed_version: str = ''
+ netbox_min_version: str = ''
+ netbox_max_version: str = ''
def get_local_plugins(plugins=None):
@@ -78,7 +80,7 @@ def get_local_plugins(plugins=None):
local_plugins = {}
# Gather all locally-installed plugins
- for plugin_name in registry['plugins']['installed']:
+ for plugin_name in settings.PLUGINS:
plugin = importlib.import_module(plugin_name)
plugin_config: PluginConfig = plugin.config
installed_version = plugin_config.version
@@ -92,15 +94,17 @@ def get_local_plugins(plugins=None):
tag_line=plugin_config.description,
description_short=plugin_config.description,
is_local=True,
- is_installed=True,
+ is_loaded=plugin_name in registry['plugins']['installed'],
installed_version=installed_version,
+ netbox_min_version=plugin_config.min_version,
+ netbox_max_version=plugin_config.max_version,
)
# Update catalog entries for local plugins, or add them to the list if not listed
for k, v in local_plugins.items():
if k in plugins:
- plugins[k].is_local = True
- plugins[k].is_installed = True
+ plugins[k].is_local = v.is_local
+ plugins[k].is_loaded = v.is_loaded
plugins[k].installed_version = v.installed_version
else:
plugins[k] = v
diff --git a/netbox/core/tables/plugins.py b/netbox/core/tables/plugins.py
index 96c612366..a7773b4de 100644
--- a/netbox/core/tables/plugins.py
+++ b/netbox/core/tables/plugins.py
@@ -2,6 +2,7 @@ import django_tables2 as tables
from django.utils.translation import gettext_lazy as _
from netbox.tables import BaseTable, columns
+from .template_code import PLUGIN_IS_INSTALLED
__all__ = (
'CatalogPluginTable',
@@ -48,12 +49,15 @@ class CatalogPluginTable(BaseTable):
verbose_name=_('Author')
)
is_local = columns.BooleanColumn(
+ false_mark=None,
verbose_name=_('Local')
)
- is_installed = columns.BooleanColumn(
- verbose_name=_('Installed')
+ is_installed = columns.TemplateColumn(
+ verbose_name=_('Active'),
+ template_code=PLUGIN_IS_INSTALLED
)
is_certified = columns.BooleanColumn(
+ false_mark=None,
verbose_name=_('Certified')
)
created_at = columns.DateTimeColumn(
diff --git a/netbox/core/tables/template_code.py b/netbox/core/tables/template_code.py
index c8f0058e7..9fc652c4c 100644
--- a/netbox/core/tables/template_code.py
+++ b/netbox/core/tables/template_code.py
@@ -14,3 +14,15 @@ OBJECTCHANGE_OBJECT = """
OBJECTCHANGE_REQUEST_ID = """
{{ value }}
"""
+
+PLUGIN_IS_INSTALLED = """
+{% if record.is_local %}
+ {% if record.is_loaded %}
+
+ {% else %}
+
+ {% endif %}
+{% else %}
+ —
+{% endif %}
+"""
diff --git a/netbox/netbox/middleware.py b/netbox/netbox/middleware.py
index b9424bd7c..4f9721430 100644
--- a/netbox/netbox/middleware.py
+++ b/netbox/netbox/middleware.py
@@ -2,6 +2,7 @@ from contextlib import ExitStack
import logging
import uuid
+import warnings
from django.conf import settings
from django.contrib import auth, messages
@@ -37,7 +38,10 @@ class CoreMiddleware:
# Apply all registered request processors
with ExitStack() as stack:
for request_processor in registry['request_processors']:
- stack.enter_context(request_processor(request))
+ try:
+ stack.enter_context(request_processor(request))
+ except Exception as e:
+ warnings.warn(f'Failed to initialize request processor {request_processor}: {e}')
response = self.get_response(request)
# Check if language cookie should be renewed
diff --git a/netbox/netbox/plugins/__init__.py b/netbox/netbox/plugins/__init__.py
index bb3280ac4..b7bb0ef9f 100644
--- a/netbox/netbox/plugins/__init__.py
+++ b/netbox/netbox/plugins/__init__.py
@@ -6,6 +6,7 @@ from django.core.exceptions import ImproperlyConfigured
from django.utils.module_loading import import_string
from packaging import version
+from core.exceptions import IncompatiblePluginError
from netbox.registry import registry
from netbox.search import register_search
from netbox.utils import register_data_backend
@@ -140,14 +141,14 @@ class PluginConfig(AppConfig):
if cls.min_version is not None:
min_version = version.parse(cls.min_version)
if current_version < min_version:
- raise ImproperlyConfigured(
+ raise IncompatiblePluginError(
f"Plugin {cls.__module__} requires NetBox minimum version {cls.min_version} (current: "
f"{netbox_version})."
)
if cls.max_version is not None:
max_version = version.parse(cls.max_version)
if current_version > max_version:
- raise ImproperlyConfigured(
+ raise IncompatiblePluginError(
f"Plugin {cls.__module__} requires NetBox maximum version {cls.max_version} (current: "
f"{netbox_version})."
)
diff --git a/netbox/netbox/settings.py b/netbox/netbox/settings.py
index bb187ac73..57b143f5f 100644
--- a/netbox/netbox/settings.py
+++ b/netbox/netbox/settings.py
@@ -12,6 +12,7 @@ from django.core.validators import URLValidator
from django.utils.module_loading import import_string
from django.utils.translation import gettext_lazy as _
+from core.exceptions import IncompatiblePluginError
from netbox.config import PARAMS as CONFIG_PARAMS
from netbox.constants import RQ_QUEUE_DEFAULT, RQ_QUEUE_HIGH, RQ_QUEUE_LOW
from netbox.plugins import PluginConfig
@@ -821,6 +822,15 @@ for plugin_name in PLUGINS:
f"__init__.py file and point to the PluginConfig subclass."
)
+ # Validate version compatibility and user-provided configuration settings and assign defaults
+ if plugin_name not in PLUGINS_CONFIG:
+ PLUGINS_CONFIG[plugin_name] = {}
+ try:
+ plugin_config.validate(PLUGINS_CONFIG[plugin_name], RELEASE.version)
+ except IncompatiblePluginError as e:
+ warnings.warn(f'Unable to load plugin {plugin_name}: {e}')
+ continue
+
# Register the plugin as installed successfully
registry['plugins']['installed'].append(plugin_name)
@@ -853,11 +863,6 @@ for plugin_name in PLUGINS:
sorted_apps = reversed(list(dict.fromkeys(reversed(INSTALLED_APPS))))
INSTALLED_APPS = list(sorted_apps)
- # Validate user-provided configuration settings and assign defaults
- if plugin_name not in PLUGINS_CONFIG:
- PLUGINS_CONFIG[plugin_name] = {}
- plugin_config.validate(PLUGINS_CONFIG[plugin_name], RELEASE.version)
-
# Add middleware
plugin_middleware = plugin_config.middleware
if plugin_middleware and type(plugin_middleware) in (list, tuple):
@@ -879,6 +884,7 @@ for plugin_name in PLUGINS:
else:
raise ImproperlyConfigured(f"events_pipline in plugin: {plugin_name} must be a list or tuple")
+
# UNSUPPORTED FUNCTIONALITY: Import any local overrides.
try:
from .local_settings import *