Skip to content

nautobot.apps

Core app functionality.

nautobot.apps.nautobot_database_ready = Signal() module-attribute

Signal sent to all installed apps and plugins after the database is ready.

Specifically this is triggered by the Django built-in post_migrate signal, i.e., after nautobot-server migrate or nautobot-server post_upgrade commands are run.

In other words, this signal is not emitted during the actual server execution; rather it is emitted when setting up the database prior to running the server.

The intended purpose of this signal is for apps and plugins that need to populate or modify the database contents (not the database schema itself!), for example to ensure the existence of certain CustomFields, Jobs, Relationships, etc.

nautobot.apps.NautobotAppConfig

Bases: NautobotConfig

Subclass of Django's built-in AppConfig class, to be used for Nautobot plugins.

Source code in nautobot/extras/plugins/__init__.py
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"
    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

        # 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

ready()

Callback after plugin app is loaded.

Source code in nautobot/extras/plugins/__init__.py
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

    # 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)

validate(user_config, nautobot_version) classmethod

Validate the user_config for baseline correctness.

Source code in nautobot/extras/plugins/__init__.py
@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