class NautobotAppConfig(NautobotConfig):
"""
Subclass of Django's built-in AppConfig class, to be used for Nautobot plugins.
"""
# Plugin metadata
author = ""
author_email = ""
description = ""
version = ""
# Root URL path under /plugins. If not set, the plugin's label will be used.
base_url = None
# Minimum/maximum compatible versions of Nautobot
min_version = None
max_version = None
# Default configuration parameters
default_settings = {}
# Mandatory configuration parameters
required_settings = []
# Middleware classes provided by the plugin
middleware = []
# Extra installed apps provided or required by the plugin. These will be registered
# along with the plugin.
installed_apps = []
# Cacheops configuration. Cache all operations by default.
caching_config = {
"*": {"ops": "all"},
}
# URL reverse lookup names, a la "plugins:myplugin:home", "plugins:myplugin:configure", "plugins:myplugin:docs"
home_view_name = None
config_view_name = None
docs_view_name = None
# Default integration paths. Plugin authors can override these to customize the paths to
# integrated components.
banner_function = "banner.banner"
custom_validators = "custom_validators.custom_validators"
datasource_contents = "datasources.datasource_contents"
filter_extensions = "filter_extensions.filter_extensions"
graphql_types = "graphql.types.graphql_types"
homepage_layout = "homepage.layout"
jinja_filters = "jinja_filters"
jobs = "jobs.jobs"
metrics = "metrics.metrics"
menu_items = "navigation.menu_items"
secrets_providers = "secrets.secrets_providers"
template_extensions = "template_content.template_extensions"
override_views = "views.override_views"
def ready(self):
"""Callback after plugin app is loaded."""
# We don't call super().ready here because we don't need or use the on-ready behavior of a core Nautobot app
# Introspect URL patterns and models to make available to the installed-plugins detail UI view.
urlpatterns = import_object(f"{self.__module__}.urls.urlpatterns")
api_urlpatterns = import_object(f"{self.__module__}.api.urls.urlpatterns")
self.features = {
"api_urlpatterns": sorted(
(urlp for urlp in (api_urlpatterns or []) if isinstance(urlp, URLPattern)),
key=lambda urlp: (urlp.name, str(urlp.pattern)),
),
"models": sorted(model._meta.verbose_name for model in self.get_models()),
"urlpatterns": sorted(
(urlp for urlp in (urlpatterns or []) if isinstance(urlp, URLPattern)),
key=lambda urlp: (urlp.name, str(urlp.pattern)),
),
}
# Register banner function (if defined)
banner_function = import_object(f"{self.__module__}.{self.banner_function}")
if banner_function is not None:
register_banner_function(banner_function)
self.features["banner"] = True
# Register model validators (if defined)
validators = import_object(f"{self.__module__}.{self.custom_validators}")
if validators is not None:
register_custom_validators(validators)
self.features["custom_validators"] = sorted(set(validator.model for validator in validators))
# Register datasource contents (if defined)
datasource_contents = import_object(f"{self.__module__}.{self.datasource_contents}")
if datasource_contents is not None:
register_datasource_contents(datasource_contents)
self.features["datasource_contents"] = datasource_contents
# Register GraphQL types (if defined)
graphql_types = import_object(f"{self.__module__}.{self.graphql_types}")
if graphql_types is not None:
register_graphql_types(graphql_types)
# Import jobs (if present)
jobs = import_object(f"{self.__module__}.{self.jobs}")
if jobs is not None:
register_jobs(jobs)
self.features["jobs"] = jobs
# Import metrics (if present)
metrics = import_object(f"{self.__module__}.{self.metrics}")
if metrics is not None:
register_metrics(metrics)
self.features["metrics"] = [] # Initialize as empty, to be filled by the signal handler
# Inject the metrics to discover into the signal handler.
signal_callback = partial(discover_metrics, metrics=metrics)
nautobot_database_ready.connect(signal_callback, sender=self)
# Register plugin navigation menu items (if defined)
menu_items = import_object(f"{self.__module__}.{self.menu_items}")
if menu_items is not None:
register_plugin_menu_items(self.verbose_name, menu_items)
self.features["nav_menu"] = menu_items
homepage_layout = import_object(f"{self.__module__}.{self.homepage_layout}")
if homepage_layout is not None:
register_homepage_panels(self.path, self.label, homepage_layout)
self.features["home_page"] = homepage_layout
# Register template content (if defined)
template_extensions = import_object(f"{self.__module__}.{self.template_extensions}")
if template_extensions is not None:
register_template_extensions(template_extensions)
self.features["template_extensions"] = sorted(set(extension.model for extension in template_extensions))
# Register custom jinja filters
try:
import_module(f"{self.__module__}.{self.jinja_filters}")
self.features["jinja_filters"] = True
except ModuleNotFoundError:
pass
# Register secrets providers (if any)
secrets_providers = import_object(f"{self.__module__}.{self.secrets_providers}")
if secrets_providers is not None:
for secrets_provider in secrets_providers:
register_secrets_provider(secrets_provider)
self.features["secrets_providers"] = secrets_providers
# Register custom filters (if any)
filter_extensions = import_object(f"{self.__module__}.{self.filter_extensions}")
if filter_extensions is not None:
register_filter_extensions(filter_extensions, self.name)
self.features["filter_extensions"] = {"filterset_fields": [], "filterform_fields": []}
for filter_extension in filter_extensions:
for filterset_field_name in filter_extension.filterset_fields.keys():
self.features["filter_extensions"]["filterset_fields"].append(
f"{filter_extension.model} -> {filterset_field_name}"
)
for filterform_field_name in filter_extension.filterform_fields.keys():
self.features["filter_extensions"]["filterform_fields"].append(
f"{filter_extension.model} -> {filterform_field_name}"
)
# Register override view (if any)
override_views = import_object(f"{self.__module__}.{self.override_views}")
if override_views is not None:
for qualified_view_name, view in override_views.items():
self.features.setdefault("overridden_views", []).append(
(qualified_view_name, f"{view.__module__}.{view.__name__}")
)
register_override_views(override_views, self.name)
@classmethod
def validate(cls, user_config, nautobot_version):
"""Validate the user_config for baseline correctness."""
plugin_name = cls.__module__
# Enforce version constraints
current_version = version.parse(nautobot_version)
if cls.min_version is not None:
min_version = version.parse(cls.min_version)
if current_version < min_version:
raise PluginImproperlyConfigured(
f"Plugin {plugin_name} requires Nautobot minimum version {cls.min_version}"
)
if cls.max_version is not None:
max_version = version.parse(cls.max_version)
if current_version > max_version:
raise PluginImproperlyConfigured(
f"Plugin {plugin_name} requires Nautobot maximum version {cls.max_version}"
)
# Mapping of {setting_name: setting_type} used to validate user configs
# TODO(jathan): This is fine for now, but as we expand the functionality
# of plugins, we'll need to consider something like pydantic or attrs.
setting_validations = {
"caching_config": dict,
"default_settings": dict,
"installed_apps": list,
"middleware": list,
"required_settings": list,
}
# Validate user settings
for setting_name, setting_type in setting_validations.items():
if not isinstance(getattr(cls, setting_name), setting_type):
raise PluginImproperlyConfigured(f"Plugin {plugin_name} {setting_name} must be a {setting_type}")
# Validate the required_settings
for setting in cls.required_settings:
if setting not in user_config:
raise PluginImproperlyConfigured(
f"Plugin {plugin_name} requires '{setting}' to be present in "
f"the PLUGINS_CONFIG['{plugin_name}'] section of your settings."
)
# Apply default configuration values
for setting, value in cls.default_settings.items():
if setting not in user_config:
user_config[setting] = value