mirror of
https://github.com/netbox-community/netbox.git
synced 2025-07-16 04:02:52 -06:00
* Skip incompatible plugins during startup and remove from PLUGINS
* Handle exceptions on request processors in incompatible plugins, and display status in Plugins page
* Revert "Handle exceptions on request processors in incompatible plugins, and display status in Plugins page"
This reverts commit d97bf2ab146114cc13d751878a17a383de0fd5f8.
* Resolve merge conflicts
* Skip incompatible plugins during startup and remove from PLUGINS
* Rename Installed column to Active, and add custom PluginActiveColumn with tooltip
* Fix is_installed
* Simplify plugin_config.validate syntax
Co-authored-by: Jeremy Stretch <jstretch@netboxlabs.com>
* Merge feature
* Revert "Merge feature"
This reverts commit d1ea60f082
.
* Undo simplification
* Add failed_to_load logic
* Use a TemplateColumn for is_installed
* Remove custom column class
* Remove merge vestige
* Simplify plugin attributes for is_installed column
* Use placeholders for false values to increase legibility of the plugins table
---------
Co-authored-by: Jeremy Stretch <jstretch@netboxlabs.com>
This commit is contained in:
parent
c35f5f829a
commit
b5d970f7bb
@ -1,2 +1,9 @@
|
|||||||
|
from django.core.exceptions import ImproperlyConfigured
|
||||||
|
|
||||||
|
|
||||||
class SyncError(Exception):
|
class SyncError(Exception):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class IncompatiblePluginError(ImproperlyConfigured):
|
||||||
|
pass
|
||||||
|
@ -65,9 +65,11 @@ class Plugin:
|
|||||||
is_certified: bool = False
|
is_certified: bool = False
|
||||||
release_latest: PluginVersion = field(default_factory=PluginVersion)
|
release_latest: PluginVersion = field(default_factory=PluginVersion)
|
||||||
release_recent_history: list[PluginVersion] = field(default_factory=list)
|
release_recent_history: list[PluginVersion] = field(default_factory=list)
|
||||||
is_local: bool = False # extra field for locally installed plugins
|
is_local: bool = False # Indicates that the plugin is listed in settings.PLUGINS (i.e. installed)
|
||||||
is_installed: bool = False
|
is_loaded: bool = False # Indicates whether the plugin successfully loaded at launch
|
||||||
installed_version: str = ''
|
installed_version: str = ''
|
||||||
|
netbox_min_version: str = ''
|
||||||
|
netbox_max_version: str = ''
|
||||||
|
|
||||||
|
|
||||||
def get_local_plugins(plugins=None):
|
def get_local_plugins(plugins=None):
|
||||||
@ -78,7 +80,7 @@ def get_local_plugins(plugins=None):
|
|||||||
local_plugins = {}
|
local_plugins = {}
|
||||||
|
|
||||||
# Gather all locally-installed 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 = importlib.import_module(plugin_name)
|
||||||
plugin_config: PluginConfig = plugin.config
|
plugin_config: PluginConfig = plugin.config
|
||||||
installed_version = plugin_config.version
|
installed_version = plugin_config.version
|
||||||
@ -92,15 +94,17 @@ def get_local_plugins(plugins=None):
|
|||||||
tag_line=plugin_config.description,
|
tag_line=plugin_config.description,
|
||||||
description_short=plugin_config.description,
|
description_short=plugin_config.description,
|
||||||
is_local=True,
|
is_local=True,
|
||||||
is_installed=True,
|
is_loaded=plugin_name in registry['plugins']['installed'],
|
||||||
installed_version=installed_version,
|
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
|
# Update catalog entries for local plugins, or add them to the list if not listed
|
||||||
for k, v in local_plugins.items():
|
for k, v in local_plugins.items():
|
||||||
if k in plugins:
|
if k in plugins:
|
||||||
plugins[k].is_local = True
|
plugins[k].is_local = v.is_local
|
||||||
plugins[k].is_installed = True
|
plugins[k].is_loaded = v.is_loaded
|
||||||
plugins[k].installed_version = v.installed_version
|
plugins[k].installed_version = v.installed_version
|
||||||
else:
|
else:
|
||||||
plugins[k] = v
|
plugins[k] = v
|
||||||
|
@ -2,6 +2,7 @@ import django_tables2 as tables
|
|||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
from netbox.tables import BaseTable, columns
|
from netbox.tables import BaseTable, columns
|
||||||
|
from .template_code import PLUGIN_IS_INSTALLED
|
||||||
|
|
||||||
__all__ = (
|
__all__ = (
|
||||||
'CatalogPluginTable',
|
'CatalogPluginTable',
|
||||||
@ -48,12 +49,15 @@ class CatalogPluginTable(BaseTable):
|
|||||||
verbose_name=_('Author')
|
verbose_name=_('Author')
|
||||||
)
|
)
|
||||||
is_local = columns.BooleanColumn(
|
is_local = columns.BooleanColumn(
|
||||||
|
false_mark=None,
|
||||||
verbose_name=_('Local')
|
verbose_name=_('Local')
|
||||||
)
|
)
|
||||||
is_installed = columns.BooleanColumn(
|
is_installed = columns.TemplateColumn(
|
||||||
verbose_name=_('Installed')
|
verbose_name=_('Active'),
|
||||||
|
template_code=PLUGIN_IS_INSTALLED
|
||||||
)
|
)
|
||||||
is_certified = columns.BooleanColumn(
|
is_certified = columns.BooleanColumn(
|
||||||
|
false_mark=None,
|
||||||
verbose_name=_('Certified')
|
verbose_name=_('Certified')
|
||||||
)
|
)
|
||||||
created_at = columns.DateTimeColumn(
|
created_at = columns.DateTimeColumn(
|
||||||
|
@ -14,3 +14,15 @@ OBJECTCHANGE_OBJECT = """
|
|||||||
OBJECTCHANGE_REQUEST_ID = """
|
OBJECTCHANGE_REQUEST_ID = """
|
||||||
<a href="{% url 'core:objectchange_list' %}?request_id={{ value }}">{{ value }}</a>
|
<a href="{% url 'core:objectchange_list' %}?request_id={{ value }}">{{ value }}</a>
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
PLUGIN_IS_INSTALLED = """
|
||||||
|
{% if record.is_local %}
|
||||||
|
{% if record.is_loaded %}
|
||||||
|
<span class="text-success"><i class="mdi mdi-check-bold"></i></span>
|
||||||
|
{% else %}
|
||||||
|
<span class="text-danger"><i class="mdi mdi-alert" data-bs-toggle="tooltip" title="Could not load plugin. Version may be incompatible. Min version: {{ record.netbox_min_version }}, max version: {{ record.netbox_max_version }}"></i></span>
|
||||||
|
{% endif %}
|
||||||
|
{% else %}
|
||||||
|
<span class="text-muted">—</span>
|
||||||
|
{% endif %}
|
||||||
|
"""
|
||||||
|
@ -2,6 +2,7 @@ from contextlib import ExitStack
|
|||||||
|
|
||||||
import logging
|
import logging
|
||||||
import uuid
|
import uuid
|
||||||
|
import warnings
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.contrib import auth, messages
|
from django.contrib import auth, messages
|
||||||
@ -37,7 +38,10 @@ class CoreMiddleware:
|
|||||||
# Apply all registered request processors
|
# Apply all registered request processors
|
||||||
with ExitStack() as stack:
|
with ExitStack() as stack:
|
||||||
for request_processor in registry['request_processors']:
|
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)
|
response = self.get_response(request)
|
||||||
|
|
||||||
# Check if language cookie should be renewed
|
# Check if language cookie should be renewed
|
||||||
|
@ -6,6 +6,7 @@ from django.core.exceptions import ImproperlyConfigured
|
|||||||
from django.utils.module_loading import import_string
|
from django.utils.module_loading import import_string
|
||||||
from packaging import version
|
from packaging import version
|
||||||
|
|
||||||
|
from core.exceptions import IncompatiblePluginError
|
||||||
from netbox.registry import registry
|
from netbox.registry import registry
|
||||||
from netbox.search import register_search
|
from netbox.search import register_search
|
||||||
from netbox.utils import register_data_backend
|
from netbox.utils import register_data_backend
|
||||||
@ -140,14 +141,14 @@ class PluginConfig(AppConfig):
|
|||||||
if cls.min_version is not None:
|
if cls.min_version is not None:
|
||||||
min_version = version.parse(cls.min_version)
|
min_version = version.parse(cls.min_version)
|
||||||
if current_version < min_version:
|
if current_version < min_version:
|
||||||
raise ImproperlyConfigured(
|
raise IncompatiblePluginError(
|
||||||
f"Plugin {cls.__module__} requires NetBox minimum version {cls.min_version} (current: "
|
f"Plugin {cls.__module__} requires NetBox minimum version {cls.min_version} (current: "
|
||||||
f"{netbox_version})."
|
f"{netbox_version})."
|
||||||
)
|
)
|
||||||
if cls.max_version is not None:
|
if cls.max_version is not None:
|
||||||
max_version = version.parse(cls.max_version)
|
max_version = version.parse(cls.max_version)
|
||||||
if current_version > max_version:
|
if current_version > max_version:
|
||||||
raise ImproperlyConfigured(
|
raise IncompatiblePluginError(
|
||||||
f"Plugin {cls.__module__} requires NetBox maximum version {cls.max_version} (current: "
|
f"Plugin {cls.__module__} requires NetBox maximum version {cls.max_version} (current: "
|
||||||
f"{netbox_version})."
|
f"{netbox_version})."
|
||||||
)
|
)
|
||||||
|
@ -12,6 +12,7 @@ from django.core.validators import URLValidator
|
|||||||
from django.utils.module_loading import import_string
|
from django.utils.module_loading import import_string
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
|
from core.exceptions import IncompatiblePluginError
|
||||||
from netbox.config import PARAMS as CONFIG_PARAMS
|
from netbox.config import PARAMS as CONFIG_PARAMS
|
||||||
from netbox.constants import RQ_QUEUE_DEFAULT, RQ_QUEUE_HIGH, RQ_QUEUE_LOW
|
from netbox.constants import RQ_QUEUE_DEFAULT, RQ_QUEUE_HIGH, RQ_QUEUE_LOW
|
||||||
from netbox.plugins import PluginConfig
|
from netbox.plugins import PluginConfig
|
||||||
@ -821,6 +822,15 @@ for plugin_name in PLUGINS:
|
|||||||
f"__init__.py file and point to the PluginConfig subclass."
|
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
|
# Register the plugin as installed successfully
|
||||||
registry['plugins']['installed'].append(plugin_name)
|
registry['plugins']['installed'].append(plugin_name)
|
||||||
|
|
||||||
@ -853,11 +863,6 @@ for plugin_name in PLUGINS:
|
|||||||
sorted_apps = reversed(list(dict.fromkeys(reversed(INSTALLED_APPS))))
|
sorted_apps = reversed(list(dict.fromkeys(reversed(INSTALLED_APPS))))
|
||||||
INSTALLED_APPS = list(sorted_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
|
# Add middleware
|
||||||
plugin_middleware = plugin_config.middleware
|
plugin_middleware = plugin_config.middleware
|
||||||
if plugin_middleware and type(plugin_middleware) in (list, tuple):
|
if plugin_middleware and type(plugin_middleware) in (list, tuple):
|
||||||
@ -879,6 +884,7 @@ for plugin_name in PLUGINS:
|
|||||||
else:
|
else:
|
||||||
raise ImproperlyConfigured(f"events_pipline in plugin: {plugin_name} must be a list or tuple")
|
raise ImproperlyConfigured(f"events_pipline in plugin: {plugin_name} must be a list or tuple")
|
||||||
|
|
||||||
|
|
||||||
# UNSUPPORTED FUNCTIONALITY: Import any local overrides.
|
# UNSUPPORTED FUNCTIONALITY: Import any local overrides.
|
||||||
try:
|
try:
|
||||||
from .local_settings import *
|
from .local_settings import *
|
||||||
|
Loading…
Reference in New Issue
Block a user