Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 8 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ A NetBox plugin that dynamically reloads plugins without requiring a server rest

- Dynamically registers plugin models that were missed during server startup
- Refreshes custom field form definitions to include newly registered models
- Refreshes tag form definitions to include newly registered models
- Helps solve integration issues between NetBox and other plugins
- No configuration required - works out of the box

Expand All @@ -14,6 +15,9 @@ A NetBox plugin that dynamically reloads plugins without requiring a server rest
| NetBox Version | Plugin Version |
|----------------|---------------|
| 4.2.x | 0.0.2 |
| 4.3.x | 4.3.x |

**Version Format**: X.X.Y where X.X = NetBox version (e.g., 4.3) and Y = plugin version increment

## Installation

Expand Down Expand Up @@ -54,9 +58,10 @@ When NetBox starts, Plugin Reloader:

1. Scans all enabled plugins for models that aren't properly registered in NetBox's feature registry
2. Registers any missed models with NetBox's registration system
3. Refreshes form field definitions to ensure they include all registered models
3. Refreshes custom field form definitions to ensure they include all registered models
4. Refreshes tag form definitions to ensure they include all registered models

This helps resolve issues where plugins might not fully integrate with NetBox due to load order problems without requiring a server restart.
This helps resolve issues where plugins might not fully integrate with NetBox due to load order problems without requiring a server restart. The reloader specifically updates custom field choices and tag choices to include newly registered plugin models.

## Contributing

Expand All @@ -71,4 +76,4 @@ This project is licensed under the MIT License - see the LICENSE file for detail
Jan Krupa <jan.krupa@cesnet.cz>

## Links
- Based on https://github.com/netbox-community/netbox/discussions/17836
- Based on https://github.com/netbox-community/netbox/discussions/17836
133 changes: 41 additions & 92 deletions netbox_plugin_reloader/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
"""

from netbox.plugins import PluginConfig

from netbox_plugin_reloader.version import __version__


Expand All @@ -19,149 +20,97 @@ class NetboxPluginReloaderConfig(PluginConfig):
description = "Dynamically reload NetBox plugins without server restart"
version = __version__
base_url = "netbox-plugin-reloader"

# Plugin configuration
default_settings = {}
required_settings = []

# NetBox version compatibility
min_version = "4.3.0"
max_version = "4.3.99"

def ready(self):
"""
Plugin initialization logic executed when Django loads the application.

This method handles the dynamic registration of plugin models and
refreshes form fields to ensure all plugins are properly loaded.
Initializes the plugin when the Django application loads.

Registers any plugin models missed during startup and refreshes form fields to include newly registered models for custom fields and tags.
"""
# Initialize parent plugin functionality
super().ready()

# Import dependencies
from core.models import ObjectType
from django.apps import apps
from django.conf import settings
from django.utils.translation import gettext_lazy as _
from extras.forms.model_forms import CustomFieldForm
from extras.forms.model_forms import CustomFieldForm, TagForm
from netbox.models.features import FEATURES_MAP, register_models
from netbox.registry import registry
from utilities.forms.fields import ContentTypeMultipleChoiceField

# Step 1: Register any plugin models missed during initial application startup
self._register_missing_plugin_models(
plugin_list=settings.PLUGINS,
app_registry=apps,
netbox_registry=registry,
feature_mixins_map=FEATURES_MAP,
model_register_function=register_models,
)
# Register missing plugin models
self._register_missing_plugin_models(settings.PLUGINS, apps, registry, FEATURES_MAP, register_models)

# Step 2: Ensure form fields for plugins are properly initialized
self._refresh_custom_field_form(
form_class=CustomFieldForm,
field_class=ContentTypeMultipleChoiceField,
object_type_class=ObjectType,
translation_function=_,
)
# Refresh form fields
self._refresh_form_field(CustomFieldForm, "custom_fields", ObjectType, ContentTypeMultipleChoiceField, _)
self._refresh_form_field(TagForm, "tags", ObjectType, ContentTypeMultipleChoiceField, _)

def _register_missing_plugin_models(
self,
plugin_list,
app_registry,
netbox_registry,
feature_mixins_map,
model_register_function,
self, plugin_list, app_registry, netbox_registry, feature_mixins_map, model_register_function
):
"""
Register plugin models that weren't properly registered during application startup.

This method scans all enabled plugins, identifies models that haven't been
registered in NetBox's feature registry, and registers them.

Args:
plugin_list: List of enabled plugin names from settings
app_registry: Django application registry
netbox_registry: NetBox's internal registry for tracking features
feature_mixins_map: Dictionary mapping feature names to mixin classes
model_register_function: Function used to register models with NetBox
Registers plugin models that were not registered during initial application startup.

Iterates through the provided list of plugin names, identifies models that are missing from the NetBox feature registry, and registers them using the supplied registration function. Prints errors encountered during processing and reports the number of models registered if any were missed.
"""
unregistered_models = []

# For each enabled plugin
for plugin_name in plugin_list:
try:
# Get the Django app configuration for this plugin
plugin_app_config = app_registry.get_app_config(plugin_name)
app_label = plugin_app_config.label

# Check each model in the plugin
for model_class in plugin_app_config.get_models():
model_name = model_class._meta.model_name

# Only register models that aren't already in the registry
if not self._is_model_registered(
app_label=app_label,
model_name=model_name,
registry=netbox_registry,
feature_mixins_map=feature_mixins_map,
):
if not self._is_model_registered(app_label, model_name, netbox_registry, feature_mixins_map):
unregistered_models.append(model_class)

except Exception as e:
# Safely handle errors with specific plugins
print(f"Error processing plugin {plugin_name}: {e}")

# Register the collected models if any were found
if unregistered_models:
model_register_function(*unregistered_models)
print(f"Plugin Reloader: Registered {len(unregistered_models)} previously missed models")

def _is_model_registered(self, app_label, model_name, registry, feature_mixins_map):
"""
Check if a model is already registered in any NetBox feature registry.

Args:
app_label: Django application label (e.g., 'dcim', 'ipam')
model_name: Model name without the app label
registry: NetBox registry containing feature registrations
feature_mixins_map: Dictionary mapping feature names to mixin classes

Determines whether a model is registered under any NetBox feature.

Returns:
bool: True if model is registered in any feature, False otherwise
True if the specified model is present in any feature registry; otherwise, False.
"""
# Check each available feature registry
for feature_name in feature_mixins_map.keys():
feature_registry = registry["model_features"][feature_name]

# If the app_label exists and the model is registered under it
if app_label in feature_registry and model_name in feature_registry[app_label]:
return True

# Model not found in any feature registry
return False
return any(
app_label in registry["model_features"][feature_name]
and model_name in registry["model_features"][feature_name][app_label]
for feature_name in feature_mixins_map.keys()
)

def _refresh_custom_field_form(self, form_class, field_class, object_type_class, translation_function):
def _refresh_form_field(self, form_class, feature_name, object_type_class, field_class, translation_function):
"""
Refresh form field definitions for custom fields.

This ensures that plugin models are properly included in form field choices
after they've been registered.

Updates a form class's object_types field to reflect models supporting a specific NetBox feature.

Args:
form_class: The CustomFieldForm class to update
field_class: Field class to use for the object_types field
object_type_class: The ObjectType model class
translation_function: Function for internationalizing strings
form_class: The form class to update.
feature_name: The NetBox feature name (e.g., "custom_fields", "tags").
object_type_class: The ContentType-like class used to query object types.
field_class: The form field class to instantiate.
translation_function: Function used to translate field labels and help texts.
"""
# Create a field that includes all models with custom_fields feature
field_labels = {
"custom_fields": ("Object types", "The type(s) of object that have this custom field"),
"tags": ("Object types", "The type(s) of object that can have this tag"),
}

label, help_text = field_labels[feature_name]

object_types_field = field_class(
label=translation_function("Object types"),
queryset=object_type_class.objects.with_feature("custom_fields"),
help_text=translation_function("The type(s) of object that have this custom field"),
label=translation_function(label),
queryset=object_type_class.objects.with_feature(feature_name),
help_text=translation_function(help_text),
)

# Update the form definition
form_class.base_fields["object_types"] = object_types_field


Expand Down
2 changes: 1 addition & 1 deletion netbox_plugin_reloader/version.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
"""Version information."""

__version__ = "4.3.0"
__version__ = "4.3.1"
1 change: 0 additions & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,6 @@ def get_version(rel_path):
'Framework :: Django :: 5.0',
'License :: OSI Approved :: Apache Software License',
'Programming Language :: Python :: 3',
'Programming Language :: Python :: 3.9',
'Programming Language :: Python :: 3.10',
'Operating System :: OS Independent',
],
Expand Down