nautobot.apps - Nautobot Documentation

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`

     43
     44
     45
     46
     47
     48
     49
     50
     51
     52
     53
     54
     55
     56
     57
     58
     59
     60
     61
     62
     63
     64
     65
     66
     67
     68
     69
     70
     71
     72
     73
     74
     75
     76
     77
     78
     79
     80
     81
     82
     83
     84
     85
     86
     87
     88
     89
     90
     91
     92
     93
     94
     95
     96
     97
     98
     99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    128
    129
    130
    131
    132
    133
    134
    135
    136
    137
    138
    139
    140
    141
    142
    143
    144
    145
    146
    147
    148
    149
    150
    151
    152
    153
    154
    155
    156
    157
    158
    159
    160
    161
    162
    163
    164
    165
    166
    167
    168
    169
    170
    171
    172
    173
    174
    175
    176
    177
    178
    179
    180
    181
    182
    183
    184
    185
    186
    187
    188
    189
    190
    191
    192
    193
    194
    195
    196
    197
    198
    199
    200
    201
    202
    203
    204
    205
    206
    207
    208
    209
    210
    211
    212
    213
    214
    215
    216
    217
    218
    219
    220
    221
    222
    223
    224
    225
    226
    227
    228
    229
    230
    231
    232
    233
    234
    235
    236
    237
    238
    239
    240
    241
    242
    243
    244
    245
    246
    247
    248
    249
    250
    251
    252
    253
    254
    255
    256
    257
    258
    259
    260
    261
    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 = []

        # Default constance configuration parameters
        constance_config = {}

        # 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)),
                ),
                "constance_config": self.constance_config,
            }

            # 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 = {
                "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():
                # user_config and constance_config take precedence
                # this is to support legacy apps that supply default_settings and constance_config
                if setting not in user_config and setting not in cls.constance_config:
                    user_config[setting] = value

### `ready()`

Callback after plugin app is loaded.

Source code in `nautobot/extras/plugins/__init__.py`

     98
     99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    128
    129
    130
    131
    132
    133
    134
    135
    136
    137
    138
    139
    140
    141
    142
    143
    144
    145
    146
    147
    148
    149
    150
    151
    152
    153
    154
    155
    156
    157
    158
    159
    160
    161
    162
    163
    164
    165
    166
    167
    168
    169
    170
    171
    172
    173
    174
    175
    176
    177
    178
    179
    180
    181
    182
    183
    184
    185
    186
    187
    188
    189
    190
    191
    192
    193
    194
    195
    196
    197
    198
    199
    200
    201
    202
    203
    204
    205
    206
    207
    208
    209
    210
    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)),
            ),
            "constance_config": self.constance_config,
        }

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

### `validate(user_config, nautobot_version)` `classmethod`

Validate the user_config for baseline correctness.

Source code in `nautobot/extras/plugins/__init__.py`

    212
    213
    214
    215
    216
    217
    218
    219
    220
    221
    222
    223
    224
    225
    226
    227
    228
    229
    230
    231
    232
    233
    234
    235
    236
    237
    238
    239
    240
    241
    242
    243
    244
    245
    246
    247
    248
    249
    250
    251
    252
    253
    254
    255
    256
    257
    258
    259
    260
    261
    @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 = {
            "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():
            # user_config and constance_config take precedence
            # this is to support legacy apps that supply default_settings and constance_config
            if setting not in user_config and setting not in cls.constance_config:
                user_config[setting] = value