diff --git a/changes/341.added b/changes/341.added new file mode 100644 index 00000000..3a5de857 --- /dev/null +++ b/changes/341.added @@ -0,0 +1 @@ +Added a "grafana disabled" view in case a user clicks on a grafana nav menu item when the grafana integration is disabled. diff --git a/changes/341.fixed b/changes/341.fixed new file mode 100644 index 00000000..adf56166 --- /dev/null +++ b/changes/341.fixed @@ -0,0 +1,4 @@ +Fixed django-constance not being upgradable due to this app accessing the database before migrations could run. +Removed conditional logic for adding grafana navigation menu items. +Fixed Nautobot v2.3 incompatibility caused by saved views not being able to determine the models' table classes. +Added exception handling for cases where diffsync is not installed, since it's marked as optional. diff --git a/changes/341.housekeeping b/changes/341.housekeeping new file mode 100644 index 00000000..c694cc1c --- /dev/null +++ b/changes/341.housekeeping @@ -0,0 +1 @@ +Fixed dev environment nautobot_config.py to fall back to constance if environment variable is not used. diff --git a/changes/341.removed b/changes/341.removed new file mode 100644 index 00000000..d31260be --- /dev/null +++ b/changes/341.removed @@ -0,0 +1 @@ +Removed all grafana integration API files since there we no API views provided by grafana integration. diff --git a/development/nautobot_config.py b/development/nautobot_config.py index 31cc69c5..f38be8ce 100644 --- a/development/nautobot_config.py +++ b/development/nautobot_config.py @@ -146,36 +146,29 @@ # | `session_cache_timeout` | Controls session cache | No | `86400` | # = Chat Platforms =================== # - Mattermost ----------------------- - "enable_mattermost": is_truthy(os.getenv("NAUTOBOT_CHATOPS_ENABLE_MATTERMOST")), "mattermost_api_token": os.environ.get("MATTERMOST_API_TOKEN"), "mattermost_url": os.environ.get("MATTERMOST_URL"), # - Microsoft Teams ------------------ - "enable_ms_teams": is_truthy(os.getenv("NAUTOBOT_CHATOPS_ENABLE_MS_TEAMS")), "microsoft_app_id": os.environ.get("MICROSOFT_APP_ID"), "microsoft_app_password": os.environ.get("MICROSOFT_APP_PASSWORD"), # - Slack ---------------------------- - "enable_slack": is_truthy(os.getenv("NAUTOBOT_CHATOPS_ENABLE_SLACK")), "slack_api_token": os.environ.get("SLACK_API_TOKEN"), "slack_app_token": os.environ.get("SLACK_APP_TOKEN"), "slack_signing_secret": os.environ.get("SLACK_SIGNING_SECRET"), "slack_slash_command_prefix": os.environ.get("SLACK_SLASH_COMMAND_PREFIX", "/"), # - Cisco Webex ---------------------- - "enable_webex": is_truthy(os.getenv("NAUTOBOT_CHATOPS_ENABLE_WEBEX")), "webex_msg_char_limit": int(os.getenv("WEBEX_MSG_CHAR_LIMIT", "7439")), "webex_signing_secret": os.environ.get("WEBEX_SIGNING_SECRET"), "webex_token": os.environ.get("WEBEX_ACCESS_TOKEN"), # = Integrations ===================== # - Cisco ACI ------------------------ - "enable_aci": is_truthy(os.getenv("NAUTOBOT_CHATOPS_ENABLE_ACI")), "aci_creds": {x: os.environ[x] for x in os.environ if "APIC" in x}, # - AWX / Ansible Tower -------------- - "enable_ansible": is_truthy(os.getenv("NAUTOBOT_CHATOPS_ENABLE_ANSIBLE")), "tower_password": os.getenv("NAUTOBOT_TOWER_PASSWORD"), "tower_uri": os.getenv("NAUTOBOT_TOWER_URI"), "tower_username": os.getenv("NAUTOBOT_TOWER_USERNAME"), "tower_verify_ssl": is_truthy(os.getenv("NAUTOBOT_TOWER_VERIFY_SSL", "true")), # - Arista CloudVision --------------- - "enable_aristacv": is_truthy(os.getenv("NAUTOBOT_CHATOPS_ENABLE_ARISTACV")), "aristacv_cvaas_url": os.environ.get("ARISTACV_CVAAS_URL"), "aristacv_cvaas_token": os.environ.get("ARISTACV_CVAAS_TOKEN"), "aristacv_cvp_host": os.environ.get("ARISTACV_CVP_HOST"), @@ -184,7 +177,6 @@ "aristacv_cvp_username": os.environ.get("ARISTACV_CVP_USERNAME"), "aristacv_on_prem": is_truthy(os.environ.get("ARISTACV_ON_PREM")), # - Grafana -------------------------- - "enable_grafana": is_truthy(os.getenv("NAUTOBOT_CHATOPS_ENABLE_GRAFANA")), "grafana_url": os.environ.get("GRAFANA_URL", ""), "grafana_api_key": os.environ.get("GRAFANA_API_KEY", ""), "grafana_default_width": 0, @@ -194,26 +186,48 @@ "grafana_org_id": 1, "grafana_default_tz": "America/Denver", # - IPFabric -------------------------- - "enable_ipfabric": is_truthy(os.getenv("NAUTOBOT_CHATOPS_ENABLE_IPFABRIC")), "ipfabric_api_token": os.environ.get("IPFABRIC_API_TOKEN"), "ipfabric_host": os.environ.get("IPFABRIC_HOST"), "ipfabric_timeout": os.environ.get("IPFABRIC_TIMEOUT", 15), "ipfabric_verify": is_truthy(os.environ.get("IPFABRIC_VERIFY", True)), # - Cisco Meraki --------------------- - "enable_meraki": is_truthy(os.getenv("NAUTOBOT_CHATOPS_ENABLE_MERAKI")), "meraki_dashboard_api_key": os.environ.get("MERAKI_API_KEY"), # - Palo Alto Panorama --------------- - "enable_panorama": is_truthy(os.getenv("NAUTOBOT_CHATOPS_ENABLE_PANORAMA")), "panorama_host": os.environ.get("PANORAMA_HOST"), "panorama_password": os.environ.get("PANORAMA_PASSWORD"), "panorama_user": os.environ.get("PANORAMA_USER"), # - Cisco NSO ------------------------ - "enable_nso": is_truthy(os.getenv("NAUTOBOT_CHATOPS_ENABLE_NSO")), "nso_url": os.environ.get("NSO_URL"), "nso_username": os.environ.get("NSO_USERNAME"), "nso_password": os.environ.get("NSO_PASSWORD"), "nso_request_timeout": os.environ.get("NSO_REQUEST_TIMEOUT", 60), }, } +if os.getenv("NAUTOBOT_CHATOPS_ENABLE_MATTERMOST", "") != "": + PLUGINS_CONFIG["nautobot_chatops"]["enable_mattermost"] = ( + is_truthy(os.getenv("NAUTOBOT_CHATOPS_ENABLE_MATTERMOST")), + ) +if os.getenv("NAUTOBOT_CHATOPS_ENABLE_MS_TEAMS", "") != "": + PLUGINS_CONFIG["nautobot_chatops"]["enable_ms_teams"] = is_truthy(os.getenv("NAUTOBOT_CHATOPS_ENABLE_MS_TEAMS")) +if os.getenv("NAUTOBOT_CHATOPS_ENABLE_SLACK", "") != "": + PLUGINS_CONFIG["nautobot_chatops"]["enable_slack"] = is_truthy(os.getenv("NAUTOBOT_CHATOPS_ENABLE_SLACK")) +if os.getenv("NAUTOBOT_CHATOPS_ENABLE_WEBEX", "") != "": + PLUGINS_CONFIG["nautobot_chatops"]["enable_webex"] = is_truthy(os.getenv("NAUTOBOT_CHATOPS_ENABLE_WEBEX")) +if os.getenv("NAUTOBOT_CHATOPS_ENABLE_ACI", "") != "": + PLUGINS_CONFIG["nautobot_chatops"]["enable_aci"] = is_truthy(os.getenv("NAUTOBOT_CHATOPS_ENABLE_ACI")) +if os.getenv("NAUTOBOT_CHATOPS_ENABLE_ANSIBLE", "") != "": + PLUGINS_CONFIG["nautobot_chatops"]["enable_ansible"] = is_truthy(os.getenv("NAUTOBOT_CHATOPS_ENABLE_ANSIBLE")) +if os.getenv("NAUTOBOT_CHATOPS_ENABLE_ARISTACV", "") != "": + PLUGINS_CONFIG["nautobot_chatops"]["enable_aristacv"] = is_truthy(os.getenv("NAUTOBOT_CHATOPS_ENABLE_ARISTACV")) +if os.getenv("NAUTOBOT_CHATOPS_ENABLE_GRAFANA", "") != "": + PLUGINS_CONFIG["nautobot_chatops"]["enable_grafana"] = is_truthy(os.getenv("NAUTOBOT_CHATOPS_ENABLE_GRAFANA")) +if os.getenv("NAUTOBOT_CHATOPS_ENABLE_IPFABRIC", "") != "": + PLUGINS_CONFIG["nautobot_chatops"]["enable_ipfabric"] = is_truthy(os.getenv("NAUTOBOT_CHATOPS_ENABLE_IPFABRIC")) +if os.getenv("NAUTOBOT_CHATOPS_ENABLE_MERAKI", "") != "": + PLUGINS_CONFIG["nautobot_chatops"]["enable_meraki"] = is_truthy(os.getenv("NAUTOBOT_CHATOPS_ENABLE_MERAKI")) +if os.getenv("NAUTOBOT_CHATOPS_ENABLE_PANORAMA", "") != "": + PLUGINS_CONFIG["nautobot_chatops"]["enable_panorama"] = is_truthy(os.getenv("NAUTOBOT_CHATOPS_ENABLE_PANORAMA")) +if os.getenv("NAUTOBOT_CHATOPS_ENABLE_NSO", "") != "": + PLUGINS_CONFIG["nautobot_chatops"]["enable_nso"] = is_truthy(os.getenv("NAUTOBOT_CHATOPS_ENABLE_NSO")) METRICS_ENABLED = is_truthy(os.getenv("NAUTOBOT_METRICS_ENABLED")) diff --git a/nautobot_chatops/api/urls.py b/nautobot_chatops/api/urls.py index f470beb1..eece0127 100644 --- a/nautobot_chatops/api/urls.py +++ b/nautobot_chatops/api/urls.py @@ -2,9 +2,8 @@ import logging -from django.urls import include, path +from django.urls import path from nautobot.apps.api import OrderedDefaultRouter -from nautobot.apps.config import get_app_settings_or_config from nautobot_chatops.api.views.generic import ( AccessGrantViewSet, @@ -13,44 +12,24 @@ NautobotChatopsRootView, ) from nautobot_chatops.api.views.lookup import AccessLookupView, UserEmailLookupView +from nautobot_chatops.api.views.mattermost import MattermostInteractionView, MattermostSlashCommandView +from nautobot_chatops.api.views.ms_teams import MSTeamsMessagesView +from nautobot_chatops.api.views.slack import SlackEventAPIView, SlackInteractionView, SlackSlashCommandView +from nautobot_chatops.api.views.webex import WebexView logger = logging.getLogger(__name__) urlpatterns = [ path("lookup/", AccessLookupView.as_view(), name="access_lookup"), path("email-lookup/", UserEmailLookupView.as_view(), name="email_lookup"), + path("slack/slash_command/", SlackSlashCommandView.as_view(), name="slack_slash_command"), + path("slack/interaction/", SlackInteractionView.as_view(), name="slack_interaction"), + path("slack/event/", SlackEventAPIView.as_view(), name="slack_event"), + path("ms_teams/messages/", MSTeamsMessagesView.as_view(), name="ms_teams_messages"), + path("webex/", WebexView.as_view(), name="webex"), + path("mattermost/slash_command/", MattermostSlashCommandView.as_view(), name="mattermost_slash_command"), + path("mattermost/interaction/", MattermostInteractionView.as_view(), name="mattermost_interaction"), ] -if get_app_settings_or_config("nautobot_chatops", "enable_slack"): - from nautobot_chatops.api.views.slack import SlackEventAPIView, SlackInteractionView, SlackSlashCommandView - - urlpatterns += [ - path("slack/slash_command/", SlackSlashCommandView.as_view(), name="slack_slash_command"), - path("slack/interaction/", SlackInteractionView.as_view(), name="slack_interaction"), - path("slack/event/", SlackEventAPIView.as_view(), name="slack_event"), - ] - -if get_app_settings_or_config("nautobot_chatops", "enable_ms_teams"): - from nautobot_chatops.api.views.ms_teams import MSTeamsMessagesView - - urlpatterns += [ - path("ms_teams/messages/", MSTeamsMessagesView.as_view(), name="ms_teams_messages"), - ] - -if get_app_settings_or_config("nautobot_chatops", "enable_webex"): - from nautobot_chatops.api.views.webex import WebexView - - urlpatterns += [ - path("webex/", WebexView.as_view(), name="webex"), - ] - -if get_app_settings_or_config("nautobot_chatops", "enable_mattermost"): - from nautobot_chatops.api.views.mattermost import MattermostInteractionView, MattermostSlashCommandView - - urlpatterns += [ - path("mattermost/slash_command/", MattermostSlashCommandView.as_view(), name="mattermost_slash_command"), - path("mattermost/interaction/", MattermostInteractionView.as_view(), name="mattermost_interaction"), - ] - router = OrderedDefaultRouter() router.APIRootView = NautobotChatopsRootView router.register("commandtoken", CommandTokenViewSet) @@ -60,5 +39,3 @@ app_name = "nautobot_chatops-api" urlpatterns += router.urls - -urlpatterns += [path("grafana/", include("nautobot_chatops.integrations.grafana.api.urls"))] diff --git a/nautobot_chatops/api/views/mattermost.py b/nautobot_chatops/api/views/mattermost.py index 25e0013f..a05b827e 100644 --- a/nautobot_chatops/api/views/mattermost.py +++ b/nautobot_chatops/api/views/mattermost.py @@ -15,6 +15,7 @@ from nautobot_chatops.metrics import signature_error_cntr from nautobot_chatops.models import CommandToken from nautobot_chatops.utils import check_and_enqueue_command +from nautobot_chatops.views import SettingsControlledViewMixin from nautobot_chatops.workers import commands_help, get_commands_registry, parse_command_string # pylint: disable=logging-fstring-interpolation @@ -67,8 +68,14 @@ def verify_signature(request): return True, "Signature is valid" +class MattermostView(SettingsControlledViewMixin, View): + """Base class for Mattermost views.""" + + enable_view_setting = "enable_mattermost" + + @method_decorator(csrf_exempt, name="dispatch") -class MattermostSlashCommandView(View): +class MattermostSlashCommandView(MattermostView): """Handle notifications from a Mattermost /command.""" http_method_names = ["post"] @@ -117,7 +124,7 @@ def post(self, request, *args, **kwargs): @method_decorator(csrf_exempt, name="dispatch") -class MattermostInteractionView(View): +class MattermostInteractionView(MattermostView): """Handle notifications resulting from a Mattermost interactive block.""" http_method_names = ["post"] diff --git a/nautobot_chatops/api/views/ms_teams.py b/nautobot_chatops/api/views/ms_teams.py index 6b95a3b3..9fb42b87 100644 --- a/nautobot_chatops/api/views/ms_teams.py +++ b/nautobot_chatops/api/views/ms_teams.py @@ -14,6 +14,7 @@ from nautobot_chatops.dispatchers.ms_teams import MSTeamsDispatcher from nautobot_chatops.utils import check_and_enqueue_command +from nautobot_chatops.views import SettingsControlledViewMixin from nautobot_chatops.workers import commands_help, get_commands_registry, parse_command_string logger = logging.getLogger(__name__) @@ -113,9 +114,10 @@ def verify_jwt_token(request_headers, request_json): @method_decorator(csrf_exempt, name="dispatch") -class MSTeamsMessagesView(View): +class MSTeamsMessagesView(SettingsControlledViewMixin, View): """Handle notifications from a Microsoft Teams bot.""" + enable_view_setting = "enable_ms_teams" http_method_names = ["post"] # pylint: disable=too-many-locals,too-many-branches,too-many-statements diff --git a/nautobot_chatops/api/views/slack.py b/nautobot_chatops/api/views/slack.py index b390e4b6..93c4e3cc 100644 --- a/nautobot_chatops/api/views/slack.py +++ b/nautobot_chatops/api/views/slack.py @@ -16,6 +16,7 @@ from nautobot_chatops.dispatchers.slack import SlackDispatcher from nautobot_chatops.metrics import signature_error_cntr from nautobot_chatops.utils import check_and_enqueue_command +from nautobot_chatops.views import SettingsControlledViewMixin from nautobot_chatops.workers import commands_help, get_commands_registry, parse_command_string # pylint: disable=logging-fstring-interpolation @@ -65,8 +66,14 @@ def verify_signature(request): return True, "Signature is valid" +class SlackView(SettingsControlledViewMixin, View): + """Base class for Slack views.""" + + enable_view_setting = "enable_slack" + + @method_decorator(csrf_exempt, name="dispatch") -class SlackSlashCommandView(View): +class SlackSlashCommandView(SlackView): """Handle notifications from a Slack /command.""" http_method_names = ["post"] @@ -115,7 +122,7 @@ def post(self, request, *args, **kwargs): @method_decorator(csrf_exempt, name="dispatch") -class SlackInteractionView(View): +class SlackInteractionView(SlackView): """Handle notifications resulting from a Slack interactive block or modal.""" http_method_names = ["post"] @@ -276,7 +283,7 @@ def post(self, request, *args, **kwargs): @method_decorator(csrf_exempt, name="dispatch") -class SlackEventAPIView(View): +class SlackEventAPIView(SlackView): """Handle notifications resulting from a mention of the Slack app.""" http_method_names = ["post"] diff --git a/nautobot_chatops/api/views/webex.py b/nautobot_chatops/api/views/webex.py index bf8e7b5d..ed003f76 100644 --- a/nautobot_chatops/api/views/webex.py +++ b/nautobot_chatops/api/views/webex.py @@ -16,6 +16,7 @@ from nautobot_chatops.dispatchers.webex import WEBEX_CONFIG, WebexDispatcher from nautobot_chatops.utils import check_and_enqueue_command +from nautobot_chatops.views import SettingsControlledViewMixin from nautobot_chatops.workers import commands_help, get_commands_registry, parse_command_string logger = logging.getLogger(__name__) @@ -76,9 +77,10 @@ def verify_signature(request): @method_decorator(csrf_exempt, name="dispatch") -class WebexView(View): +class WebexView(SettingsControlledViewMixin, View): """Handle all supported inbound notifications from Webex.""" + enable_view_setting = "enable_webex" http_method_names = ["post"] # pylint: disable=too-many-locals,too-many-return-statements,too-many-branches diff --git a/nautobot_chatops/integrations/grafana/api/__init__.py b/nautobot_chatops/integrations/grafana/api/__init__.py deleted file mode 100644 index 590bacd3..00000000 --- a/nautobot_chatops/integrations/grafana/api/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""Base module for nautobot_chatops.integrations.grafana Provider.""" diff --git a/nautobot_chatops/integrations/grafana/api/urls.py b/nautobot_chatops/integrations/grafana/api/urls.py deleted file mode 100644 index ef337c21..00000000 --- a/nautobot_chatops/integrations/grafana/api/urls.py +++ /dev/null @@ -1,15 +0,0 @@ -"""Django urlpatterns declaration for nautobot_chatops.integrations.grafana app.""" - -from nautobot.apps.api import OrderedDefaultRouter -from nautobot.apps.config import get_app_settings_or_config - -from nautobot_chatops.integrations.grafana.api.views.generic import NautobotPluginChatopsGrafanaRootView - -urlpatterns = [] -if get_app_settings_or_config("nautobot_chatops", "enable_grafana"): - router = OrderedDefaultRouter() - router.APIRootView = NautobotPluginChatopsGrafanaRootView - - app_name = "nautobot_chatops.grafana-api" - - urlpatterns += router.urls diff --git a/nautobot_chatops/integrations/grafana/api/views/__init__.py b/nautobot_chatops/integrations/grafana/api/views/__init__.py deleted file mode 100644 index 752d6639..00000000 --- a/nautobot_chatops/integrations/grafana/api/views/__init__.py +++ /dev/null @@ -1,5 +0,0 @@ -"""API Views module for the nautobot_chatops.integrations.grafana Nautobot App. - -The views implemented in this module act as endpoints for various chat platforms -to send requests and notifications to. -""" diff --git a/nautobot_chatops/integrations/grafana/api/views/generic.py b/nautobot_chatops/integrations/grafana/api/views/generic.py deleted file mode 100644 index 62d47b05..00000000 --- a/nautobot_chatops/integrations/grafana/api/views/generic.py +++ /dev/null @@ -1,11 +0,0 @@ -"""API Views for Nautobot App Chatops Grafana.""" - -from rest_framework.routers import APIRootView - - -class NautobotPluginChatopsGrafanaRootView(APIRootView): - """Nautobot Chatops Grafana API root view.""" - - def get_view_name(self): - """Return name for API Root.""" - return "Grafana" diff --git a/nautobot_chatops/integrations/grafana/diffsync/sync.py b/nautobot_chatops/integrations/grafana/diffsync/sync.py index b7b00149..c69a7c0b 100644 --- a/nautobot_chatops/integrations/grafana/diffsync/sync.py +++ b/nautobot_chatops/integrations/grafana/diffsync/sync.py @@ -2,16 +2,22 @@ from typing import Union -from diffsync import DiffSyncFlags - -from nautobot_chatops.integrations.grafana.diffsync.models import ( - GrafanaDashboard, - GrafanaPanel, - GrafanaVariable, - NautobotDashboard, - NautobotPanel, - NautobotVariable, -) +try: + from diffsync import DiffSyncFlags + + DIFFSYNC_INSTALLED = True +except ImportError: + DIFFSYNC_INSTALLED = False + +if DIFFSYNC_INSTALLED: + from nautobot_chatops.integrations.grafana.diffsync.models import ( + GrafanaDashboard, + GrafanaPanel, + GrafanaVariable, + NautobotDashboard, + NautobotPanel, + NautobotVariable, + ) from nautobot_chatops.integrations.grafana.grafana import handler from nautobot_chatops.integrations.grafana.models import Dashboard, Panel, PanelVariable @@ -21,7 +27,13 @@ def run_dashboard_sync(overwrite: bool = False) -> Union[str, None]: Args: overwrite (bool): Overwrite Nautobot data and delete records that are no longer in Grafana. + + Raises: + ImportError: If DiffSync is not installed. """ + if not DIFFSYNC_INSTALLED: + raise ImportError("DiffSync is not installed. Please install DiffSync to use the grafana integration.") + df_flags = DiffSyncFlags.NONE if overwrite else DiffSyncFlags.SKIP_UNMATCHED_DST # Fetch dashboards from the Grafana API @@ -51,7 +63,13 @@ def run_panels_sync(dashboard: Dashboard, overwrite: bool = False) -> Union[str, Args: dashboard (nautobot_chatops.integrations.grafana.models.Dashboard): The dashboard we are going to do a diffsync with. overwrite (bool): Overwrite Nautobot data and delete records that are no longer in Grafana. + + Raises: + ImportError: If DiffSync is not installed. """ + if not DIFFSYNC_INSTALLED: + raise ImportError("DiffSync is not installed. Please install DiffSync to use the grafana integration.") + df_flags = DiffSyncFlags.NONE if overwrite else DiffSyncFlags.SKIP_UNMATCHED_DST # Fetch panels from the Grafana API @@ -81,7 +99,13 @@ def run_variables_sync(dashboard: Dashboard, overwrite: bool = False) -> Union[s Args: dashboard (nautobot_chatops.integrations.grafana.models.Dashboard): The dashboard we are going to do a diffsync with. overwrite (bool): Overwrite Nautobot data and delete records that are no longer in Grafana. + + Raises: + ImportError: If DiffSync is not installed. """ + if not DIFFSYNC_INSTALLED: + raise ImportError("DiffSync is not installed. Please install DiffSync to use the grafana integration.") + df_flags = DiffSyncFlags.NONE if overwrite else DiffSyncFlags.SKIP_UNMATCHED_DST # Fetch panels from the Grafana API diff --git a/nautobot_chatops/integrations/grafana/navigation.py b/nautobot_chatops/integrations/grafana/navigation.py index 922fbf3a..22ce676e 100644 --- a/nautobot_chatops/integrations/grafana/navigation.py +++ b/nautobot_chatops/integrations/grafana/navigation.py @@ -4,7 +4,7 @@ items = [ NavMenuItem( - link="plugins:nautobot_chatops:grafanadashboards", + link="plugins:nautobot_chatops:grafanadashboard_list", permissions=["nautobot_chatops.dashboards_read"], name="Grafana Dashboards", buttons=( @@ -15,7 +15,7 @@ ), ), NavMenuItem( - link="plugins:nautobot_chatops:grafanapanel", + link="plugins:nautobot_chatops:grafanapanel_list", permissions=["nautobot_chatops.panel_read"], name="Grafana Panels", buttons=( @@ -26,7 +26,7 @@ ), ), NavMenuItem( - link="plugins:nautobot_chatops:grafanapanelvariables", + link="plugins:nautobot_chatops:grafanapanelvariable_list", permissions=["nautobot_chatops.panelvariables_read"], name="Grafana Variables", buttons=( diff --git a/nautobot_chatops/integrations/grafana/tables.py b/nautobot_chatops/integrations/grafana/tables.py index 9e3dfa8f..72f4e7d7 100644 --- a/nautobot_chatops/integrations/grafana/tables.py +++ b/nautobot_chatops/integrations/grafana/tables.py @@ -3,29 +3,29 @@ from django_tables2 import BooleanColumn, Column, TemplateColumn from nautobot.core.tables import BaseTable, ButtonsColumn, ToggleColumn -from nautobot_chatops.integrations.grafana.models import Dashboard, Panel, PanelVariable +from nautobot_chatops.integrations.grafana.models import GrafanaDashboard, GrafanaPanel, GrafanaPanelVariable -class DashboardViewTable(BaseTable): # pylint: disable=nb-sub-class-name +class GrafanaDashboardTable(BaseTable): # pylint: disable=nb-sub-class-name """Table for rendering panels for dashboards in the grafana app.""" pk = ToggleColumn() - actions = ButtonsColumn(Dashboard, buttons=("changelog", "edit", "delete")) + actions = ButtonsColumn(GrafanaDashboard, buttons=("changelog", "edit", "delete")) class Meta(BaseTable.Meta): # pylint: disable=too-few-public-methods """Meta for class DashboardViewTable.""" - model = Dashboard + model = GrafanaDashboard fields = ("pk", "dashboard_slug", "dashboard_uid", "friendly_name", "actions") -class PanelViewTable(BaseTable): # pylint: disable=nb-sub-class-name +class GrafanaPanelTable(BaseTable): # pylint: disable=nb-sub-class-name """Table for rendering panels for dashboards in the grafana app.""" pk = ToggleColumn() - actions = ButtonsColumn(Panel, buttons=("changelog", "edit", "delete")) + actions = ButtonsColumn(GrafanaPanel, buttons=("changelog", "edit", "delete")) chat_command = TemplateColumn( template_code="/grafana get-{{ record.command_name }}", @@ -36,16 +36,16 @@ class PanelViewTable(BaseTable): # pylint: disable=nb-sub-class-name class Meta(BaseTable.Meta): # pylint: disable=too-few-public-methods """Meta for class PanelViewTable.""" - model = Panel + model = GrafanaPanel fields = ("pk", "chat_command", "command_name", "friendly_name", "panel_id", "dashboard", "active", "actions") -class PanelVariableViewTable(BaseTable): # pylint: disable=nb-sub-class-name +class GrafanaPanelVariableTable(BaseTable): # pylint: disable=nb-sub-class-name """Table for rendering panel variables for dashboards in the grafana app.""" pk = ToggleColumn() - actions = ButtonsColumn(PanelVariable, buttons=("changelog", "edit", "delete")) + actions = ButtonsColumn(GrafanaPanelVariable, buttons=("changelog", "edit", "delete")) value = TemplateColumn( template_code=( "{% if record.value %}
{{ record.value }}
{% else %}{{ record.value}}{% endif %}" @@ -58,7 +58,7 @@ class PanelVariableViewTable(BaseTable): # pylint: disable=nb-sub-class-name class Meta(BaseTable.Meta): # pylint: disable=too-few-public-methods """Meta for class PanelVariableViewTable.""" - model = PanelVariable + model = GrafanaPanelVariable fields = [ "pk", "panel", diff --git a/nautobot_chatops/integrations/grafana/urls.py b/nautobot_chatops/integrations/grafana/urls.py index 65a44008..ed237652 100644 --- a/nautobot_chatops/integrations/grafana/urls.py +++ b/nautobot_chatops/integrations/grafana/urls.py @@ -3,7 +3,7 @@ from django.urls import path from nautobot.extras.views import ObjectChangeLogView -from nautobot_chatops.integrations.grafana.models import Dashboard, Panel, PanelVariable +from nautobot_chatops.integrations.grafana.models import GrafanaDashboard, GrafanaPanel, GrafanaPanelVariable from nautobot_chatops.integrations.grafana.views import ( DashboardBulkEditView, Dashboards, @@ -33,12 +33,12 @@ urlpatterns = [ # Dashboard specific views. - path("grafana/dashboards/", Dashboards.as_view(), name="grafanadashboards"), + path("grafana/dashboards/", Dashboards.as_view(), name="grafanadashboard_list"), path( "dashboards//changelog/", ObjectChangeLogView.as_view(), name="grafanadashboard_changelog", - kwargs={"model": Dashboard}, + kwargs={"model": GrafanaDashboard}, ), path("grafana/dashboards/add/", DashboardsCreate.as_view(), name="grafanadashboard_add"), path("grafana/dashboards/sync/", DashboardsSync.as_view(), name="grafanadashboard_sync"), @@ -48,12 +48,12 @@ path("grafana/dashboards/delete/", DashboardsBulkDeleteView.as_view(), name="grafanadashboard_bulk_delete"), path("grafana/dashboards/import/", DashboardsBulkImportView.as_view(), name="grafanadashboard_import"), # Panel specific views. - path("grafana/panels/", Panels.as_view(), name="grafanapanel"), + path("grafana/panels/", Panels.as_view(), name="grafanapanel_list"), path( "panels//changelog/", ObjectChangeLogView.as_view(), name="grafanapanel_changelog", - kwargs={"model": Panel}, + kwargs={"model": GrafanaPanel}, ), path("grafana/panels/add/", PanelsCreate.as_view(), name="grafanapanel_add"), path("grafana/panels/sync/", PanelsSync.as_view(), name="grafanapanel_sync"), @@ -63,12 +63,12 @@ path("grafana/panels/delete/", PanelsBulkDeleteView.as_view(), name="grafanapanel_bulk_delete"), path("grafana/panels/import/", PanelsBulkImportView.as_view(), name="grafanapanel_import"), # Panel-variables specific views. - path("grafana/variables/", Variables.as_view(), name="grafanapanelvariables"), + path("grafana/variables/", Variables.as_view(), name="grafanapanelvariable_list"), path( "variables//changelog/", ObjectChangeLogView.as_view(), name="grafanapanelvariable_changelog", - kwargs={"model": PanelVariable}, + kwargs={"model": GrafanaPanelVariable}, ), path("grafana/variables/add/", VariablesCreate.as_view(), name="grafanapanelvariable_add"), path("grafana/variables/sync/", VariablesSync.as_view(), name="grafanapanelvariable_sync"), diff --git a/nautobot_chatops/integrations/grafana/views.py b/nautobot_chatops/integrations/grafana/views.py index 8ebeb6a1..1c8b0cc6 100644 --- a/nautobot_chatops/integrations/grafana/views.py +++ b/nautobot_chatops/integrations/grafana/views.py @@ -4,9 +4,13 @@ to send requests and notifications to. """ +import logging + from django.contrib import messages -from django.contrib.auth.mixins import PermissionRequiredMixin +from django.contrib.auth.mixins import LoginRequiredMixin, PermissionRequiredMixin from django.shortcuts import redirect, render, reverse +from django.template.response import TemplateResponse +from nautobot.apps.config import get_app_settings_or_config from nautobot.core.forms import ConfirmationForm from nautobot.core.views.generic import ( BulkDeleteView, @@ -17,7 +21,6 @@ ObjectListView, ) -from nautobot_chatops.integrations.grafana.diffsync.sync import run_dashboard_sync, run_panels_sync, run_variables_sync from nautobot_chatops.integrations.grafana.filters import DashboardFilter, PanelFilter, VariableFilter from nautobot_chatops.integrations.grafana.forms import ( DashboardBulkEditForm, @@ -32,22 +35,63 @@ PanelVariablesForm, PanelVariablesSyncForm, ) -from nautobot_chatops.integrations.grafana.grafana import handler -from nautobot_chatops.integrations.grafana.models import Dashboard, Panel, PanelVariable -from nautobot_chatops.integrations.grafana.tables import DashboardViewTable, PanelVariableViewTable, PanelViewTable +from nautobot_chatops.integrations.grafana.models import GrafanaDashboard, GrafanaPanel, GrafanaPanelVariable +from nautobot_chatops.integrations.grafana.tables import ( + GrafanaDashboardTable, + GrafanaPanelTable, + GrafanaPanelVariableTable, +) + +logger = logging.getLogger(__name__) + + +try: + from nautobot_chatops.integrations.grafana.diffsync.sync import ( + run_dashboard_sync, + run_panels_sync, + run_variables_sync, + ) + from nautobot_chatops.integrations.grafana.grafana import handler + + GRAFANA_DEPENDENCIES_INSTALLED = True + GRAFANA_DEPENDENCIES_EXCEPTION = None +except ImportError as exc: + logger.warning("Grafana integration dependencies are missing. Grafana views will be disabled.") + GRAFANA_DEPENDENCIES_INSTALLED = False + GRAFANA_DEPENDENCIES_EXCEPTION = exc + # ------------------------------------------------------------------------------------- # Dashboard Specific Views # ------------------------------------------------------------------------------------- -class Dashboards(ObjectListView): +class GrafanaViewMixin(LoginRequiredMixin): + """View mixin for Grafana views to toggle views based on constance setting.""" + + def dispatch(self, request, *args, **kwargs): + """Dispatch method for Grafana views.""" + if not request.user.is_authenticated: + return self.handle_no_permission() + if not get_app_settings_or_config("nautobot_chatops", "enable_grafana"): + return TemplateResponse( + request=self.request, + template="nautobot_chatops_grafana/grafana_disabled.html", + context={}, + ) + if not GRAFANA_DEPENDENCIES_INSTALLED: + logger.error("Grafana integration dependencies are missing. Grafana views will be disabled.") + raise GRAFANA_DEPENDENCIES_EXCEPTION + return super().dispatch(request, *args, **kwargs) + + +class Dashboards(GrafanaViewMixin, ObjectListView): """View for showing dashboard configuration.""" - queryset = Dashboard.objects.all() + queryset = GrafanaDashboard.objects.all() filterset = DashboardFilter filterset_form = DashboardsFilterForm - table = DashboardViewTable + table = GrafanaDashboardTable action_buttons = ("add", "import") template_name = "nautobot_chatops_grafana/dashboard_list.html" @@ -56,14 +100,14 @@ def get_required_permission(self): return "nautobot_chatops.dashboard_read" -class DashboardsCreate(PermissionRequiredMixin, ObjectEditView): +class DashboardsCreate(GrafanaViewMixin, PermissionRequiredMixin, ObjectEditView): """View for creating a new Dashboard.""" permission_required = "nautobot_chatops.dashboard_add" - model = Dashboard - queryset = Dashboard.objects.all() + model = GrafanaDashboard + queryset = GrafanaDashboard.objects.all() model_form = DashboardsForm - default_return_url = "plugins:nautobot_chatops:grafanadashboards" + default_return_url = "plugins:nautobot_chatops:grafanadashboard_list" class DashboardsEdit(DashboardsCreate): @@ -72,11 +116,11 @@ class DashboardsEdit(DashboardsCreate): permission_required = "nautobot_chatops.dashboard_edit" -class DashboardsSync(PermissionRequiredMixin, ObjectDeleteView): +class DashboardsSync(GrafanaViewMixin, PermissionRequiredMixin, ObjectDeleteView): """View for syncing Grafana Dashboards with the Grafana API.""" permission_required = "nautobot_chatops.dashboard_sync" - default_return_url = "plugins:nautobot_chatops:grafanadashboards" + default_return_url = "plugins:nautobot_chatops:grafanadashboard_list" def get(self, request, **kwargs): """Get request for the Dashboard Sync view.""" @@ -86,7 +130,7 @@ def get(self, request, **kwargs): { "form": ConfirmationForm(initial=request.GET), "grafana_url": handler.config.grafana_url, - "return_url": reverse("plugins:nautobot_chatops:grafanadashboards"), + "return_url": reverse("plugins:nautobot_chatops:grafanadashboard_list"), }, ) @@ -104,44 +148,44 @@ def post(self, request, **kwargs): else: messages.success(request, "Grafana Dashboards synchronization complete!") - return redirect(reverse("plugins:nautobot_chatops:grafanadashboards")) + return redirect(reverse("plugins:nautobot_chatops:grafanadashboard_list")) -class DashboardsDelete(PermissionRequiredMixin, ObjectDeleteView): +class DashboardsDelete(GrafanaViewMixin, PermissionRequiredMixin, ObjectDeleteView): """View for deleting one or more Dashboard records.""" - queryset = Dashboard.objects.all() + queryset = GrafanaDashboard.objects.all() permission_required = "nautobot_chatops.dashboard_delete" - default_return_url = "plugins:nautobot_chatops:grafanadashboards" + default_return_url = "plugins:nautobot_chatops:grafanadashboard_list" -class DashboardsBulkImportView(BulkImportView): +class DashboardsBulkImportView(GrafanaViewMixin, BulkImportView): """View for bulk import of eox notices.""" - queryset = Dashboard.objects.all() - table = DashboardViewTable - default_return_url = "plugins:nautobot_chatops:grafanadashboards" + queryset = GrafanaDashboard.objects.all() + table = GrafanaDashboardTable + default_return_url = "plugins:nautobot_chatops:grafanadashboard_list" -class DashboardsBulkDeleteView(BulkDeleteView): +class DashboardsBulkDeleteView(GrafanaViewMixin, BulkDeleteView): """View for deleting one or more Dashboard records.""" - queryset = Dashboard.objects.all() - table = DashboardViewTable + queryset = GrafanaDashboard.objects.all() + table = GrafanaDashboardTable bulk_delete_url = "plugins:nautobot_chatops:grafanadashboard_bulk_delete" - default_return_url = "plugins:nautobot_chatops:grafanadashboards" + default_return_url = "plugins:nautobot_chatops:grafanadashboard_list" def get_required_permission(self): """Return required delete permission.""" return "nautobot_chatops.dashboard_delete" -class DashboardBulkEditView(BulkEditView): +class DashboardBulkEditView(GrafanaViewMixin, BulkEditView): """View for editing one or more Dashboard records.""" - queryset = Dashboard.objects.all() + queryset = GrafanaDashboard.objects.all() filterset = DashboardFilter - table = DashboardViewTable + table = GrafanaDashboardTable form = DashboardBulkEditForm bulk_edit_url = "plugins:nautobot_chatops:grafanadashboard_bulk_edit" @@ -155,13 +199,13 @@ def get_required_permission(self): # ------------------------------------------------------------------------------------- -class Panels(ObjectListView): +class Panels(GrafanaViewMixin, ObjectListView): """View for showing panels configuration.""" - queryset = Panel.objects.all() + queryset = GrafanaPanel.objects.all() filterset = PanelFilter filterset_form = PanelsFilterForm - table = PanelViewTable + table = GrafanaPanelTable action_buttons = ("add", "import") template_name = "nautobot_chatops_grafana/panel_list.html" @@ -170,14 +214,14 @@ def get_required_permission(self): return "nautobot_chatops.panel_read" -class PanelsCreate(PermissionRequiredMixin, ObjectEditView): +class PanelsCreate(GrafanaViewMixin, PermissionRequiredMixin, ObjectEditView): """View for creating a new Panel.""" permission_required = "nautobot_chatops.panel_add" - model = Panel - queryset = Panel.objects.all() + model = GrafanaPanel + queryset = GrafanaPanel.objects.all() model_form = PanelsForm - default_return_url = "plugins:nautobot_chatops:grafanapanel" + default_return_url = "plugins:nautobot_chatops:grafanapanel_list" class PanelsEdit(PanelsCreate): @@ -186,15 +230,15 @@ class PanelsEdit(PanelsCreate): permission_required = "nautobot_chatops.panel_edit" -class PanelsSync(PermissionRequiredMixin, ObjectEditView): +class PanelsSync(GrafanaViewMixin, PermissionRequiredMixin, ObjectEditView): """View for synchronizing data between the Grafana Dashboard Panels and Nautobot.""" permission_required = "nautobot_chatops.panel_sync" - model = Panel - queryset = Panel.objects.all() + model = GrafanaPanel + queryset = GrafanaPanel.objects.all() model_form = PanelsSyncForm template_name = "nautobot_chatops_grafana/panels_sync.html" - default_return_url = "plugins:nautobot_chatops:grafanapanel" + default_return_url = "plugins:nautobot_chatops:grafanapanel_list" def get_permission_required(self): """Permissions over-rride for the Panels Sync view.""" @@ -205,9 +249,9 @@ def post(self, request, *args, **kwargs): dashboard_pk = request.POST.get("dashboard") if not dashboard_pk: messages.error(request, "Unable to determine Grafana Dashboard!") - return redirect(reverse("plugins:nautobot_chatops:grafanapanel")) + return redirect(reverse("plugins:nautobot_chatops:grafanapanel_list")) - dashboard = Dashboard.objects.get(pk=dashboard_pk) + dashboard = GrafanaDashboard.objects.get(pk=dashboard_pk) sync_data = run_panels_sync(dashboard, request.POST.get("delete") == "true") if not sync_data: @@ -215,44 +259,44 @@ def post(self, request, *args, **kwargs): else: messages.success(request, "Grafana Dashboards synchronization complete!") - return redirect(reverse("plugins:nautobot_chatops:grafanapanel")) + return redirect(reverse("plugins:nautobot_chatops:grafanapanel_list")) -class PanelsDelete(PermissionRequiredMixin, ObjectDeleteView): +class PanelsDelete(GrafanaViewMixin, PermissionRequiredMixin, ObjectDeleteView): """View for deleting one or more Panel records.""" - queryset = Panel.objects.all() + queryset = GrafanaPanel.objects.all() permission_required = "nautobot_chatops.panel_delete" - default_return_url = "plugins:nautobot_chatops:grafanapanel" + default_return_url = "plugins:nautobot_chatops:grafanapanel_list" -class PanelsBulkImportView(BulkImportView): +class PanelsBulkImportView(GrafanaViewMixin, BulkImportView): """View for bulk import of Panels.""" - queryset = Panel.objects.all() - table = PanelViewTable - default_return_url = "plugins:nautobot_chatops:grafanapanel" + queryset = GrafanaPanel.objects.all() + table = GrafanaPanelTable + default_return_url = "plugins:nautobot_chatops:grafanapanel_list" -class PanelsBulkDeleteView(BulkDeleteView): +class PanelsBulkDeleteView(GrafanaViewMixin, BulkDeleteView): """View for deleting one or more Panels records.""" - queryset = Panel.objects.all() - table = PanelViewTable + queryset = GrafanaPanel.objects.all() + table = GrafanaPanelTable bulk_delete_url = "plugins:nautobot_chatops:grafanapanel_bulk_delete" - default_return_url = "plugins:nautobot_chatops:grafanapanel" + default_return_url = "plugins:nautobot_chatops:grafanapanel_list" def get_required_permission(self): """Return required delete permission.""" return "nautobot_chatops.panel_delete" -class PanelsBulkEditView(BulkEditView): +class PanelsBulkEditView(GrafanaViewMixin, BulkEditView): """View for editing one or more Panels records.""" - queryset = Panel.objects.all() + queryset = GrafanaPanel.objects.all() filterset = PanelsFilterForm - table = PanelViewTable + table = GrafanaPanelTable form = PanelsBulkEditForm bulk_edit_url = "plugins:nautobot_chatops:grafanapanel_bulk_edit" @@ -266,13 +310,13 @@ def get_required_permission(self): # ------------------------------------------------------------------------------------- -class Variables(ObjectListView): +class Variables(GrafanaViewMixin, ObjectListView): """View for showing panel-variables configuration.""" - queryset = PanelVariable.objects.all() + queryset = GrafanaPanelVariable.objects.all() filterset = VariableFilter filterset_form = PanelVariablesFilterForm - table = PanelVariableViewTable + table = GrafanaPanelVariableTable action_buttons = ("add", "import") template_name = "nautobot_chatops_grafana/variable_list.html" @@ -281,14 +325,14 @@ def get_required_permission(self): return "nautobot_chatops.panelvariable_read" -class VariablesCreate(PermissionRequiredMixin, ObjectEditView): +class VariablesCreate(GrafanaViewMixin, PermissionRequiredMixin, ObjectEditView): """View for creating a new Variable.""" permission_required = "nautobot_chatops.panelvariable_add" - model = PanelVariable - queryset = PanelVariable.objects.all() + model = GrafanaPanelVariable + queryset = GrafanaPanelVariable.objects.all() model_form = PanelVariablesForm - default_return_url = "plugins:nautobot_chatops:grafanapanelvariables" + default_return_url = "plugins:nautobot_chatops:grafanapanelvariable_list" class VariablesEdit(VariablesCreate): @@ -297,41 +341,41 @@ class VariablesEdit(VariablesCreate): permission_required = "nautobot_chatops.panelvariable_edit" -class VariablesDelete(PermissionRequiredMixin, ObjectDeleteView): +class VariablesDelete(GrafanaViewMixin, PermissionRequiredMixin, ObjectDeleteView): """View for deleting one or more Variable records.""" - queryset = PanelVariable.objects.all() + queryset = GrafanaPanelVariable.objects.all() permission_required = "nautobot_chatops.panelvariable_delete" - default_return_url = "plugins:nautobot_chatops:grafanapanelvariables" + default_return_url = "plugins:nautobot_chatops:grafanapanelvariable_list" -class VariablesBulkImportView(BulkImportView): +class VariablesBulkImportView(GrafanaViewMixin, BulkImportView): """View for bulk import of Variables.""" - queryset = PanelVariable.objects.all() - table = PanelVariableViewTable - default_return_url = "plugins:nautobot_chatops:grafanapanelvariables" + queryset = GrafanaPanelVariable.objects.all() + table = GrafanaPanelVariableTable + default_return_url = "plugins:nautobot_chatops:grafanapanelvariable_list" -class VariablesBulkDeleteView(BulkDeleteView): +class VariablesBulkDeleteView(GrafanaViewMixin, BulkDeleteView): """View for deleting one or more Variable records.""" - queryset = PanelVariable.objects.all() - table = PanelVariableViewTable + queryset = GrafanaPanelVariable.objects.all() + table = GrafanaPanelVariableTable bulk_delete_url = "plugins:nautobot_chatops:grafanapanelvariable_bulk_delete" - default_return_url = "plugins:nautobot_chatops:grafanapanelvariables" + default_return_url = "plugins:nautobot_chatops:grafanapanelvariable_list" def get_required_permission(self): """Return required delete permission.""" return "nautobot_chatops.panelvariable_delete" -class VariablesBulkEditView(BulkEditView): +class VariablesBulkEditView(GrafanaViewMixin, BulkEditView): """View for editing one or more Variable records.""" - queryset = PanelVariable.objects.all() + queryset = GrafanaPanelVariable.objects.all() filterset = PanelVariablesFilterForm - table = PanelVariableViewTable + table = GrafanaPanelVariableTable form = PanelVariablesBulkEditForm bulk_edit_url = "plugins:nautobot_chatops:grafanapanelvariable_bulk_edit" @@ -340,15 +384,15 @@ def get_required_permission(self): return "nautobot_chatops.panelvariable_edit" -class VariablesSync(PermissionRequiredMixin, ObjectEditView): +class VariablesSync(GrafanaViewMixin, PermissionRequiredMixin, ObjectEditView): """View for synchronizing data between the Grafana Dashboard Variables and Nautobot.""" permission_required = "nautobot_chatops.panelvariable_sync" - model = PanelVariable - queryset = PanelVariable.objects.all() + model = GrafanaPanelVariable + queryset = GrafanaPanelVariable.objects.all() model_form = PanelVariablesSyncForm template_name = "nautobot_chatops_grafana/variables_sync.html" - default_return_url = "plugins:nautobot_chatops:grafanapanelvariables" + default_return_url = "plugins:nautobot_chatops:grafanapanelvariable_list" def get_permission_required(self): """Permissions over-ride for the Panels Sync view.""" @@ -359,9 +403,9 @@ def post(self, request, *args, **kwargs): dashboard_pk = request.POST.get("dashboard") if not dashboard_pk: messages.error(request, "Unable to determine Grafana Dashboard!") - return redirect(reverse("plugins:nautobot_chatops:grafanapanelvariables")) + return redirect(reverse("plugins:nautobot_chatops:grafanapanelvariable_list")) - dashboard = Dashboard.objects.get(pk=dashboard_pk) + dashboard = GrafanaDashboard.objects.get(pk=dashboard_pk) sync_data = run_variables_sync(dashboard, request.POST.get("delete") == "true") if not sync_data: @@ -369,4 +413,4 @@ def post(self, request, *args, **kwargs): else: messages.success(request, "Grafana Dashboard Variable synchronization complete!") - return redirect(reverse("plugins:nautobot_chatops:grafanapanelvariables")) + return redirect(reverse("plugins:nautobot_chatops:grafanapanelvariable_list")) diff --git a/nautobot_chatops/models.py b/nautobot_chatops/models.py index d9d29ffc..561adfce 100644 --- a/nautobot_chatops/models.py +++ b/nautobot_chatops/models.py @@ -23,9 +23,7 @@ COMMAND_TOKEN_COMMENT_HELP_TEXT, COMMAND_TOKEN_TOKEN_HELP_TEXT, ) -from .integrations.grafana.models import Dashboard as GrafanaDashboard -from .integrations.grafana.models import Panel as GrafanaPanel -from .integrations.grafana.models import PanelVariable as GrafanaPanelVariable +from .integrations.grafana.models import GrafanaDashboard, GrafanaPanel, GrafanaPanelVariable class CommandLog(PrimaryModel): # pylint: disable=nb-string-field-blank-null diff --git a/nautobot_chatops/navigation.py b/nautobot_chatops/navigation.py index 3bd9ec7b..f84887a9 100644 --- a/nautobot_chatops/navigation.py +++ b/nautobot_chatops/navigation.py @@ -1,13 +1,8 @@ """App additions to the Nautobot navigation menu.""" -from nautobot.apps.config import get_app_settings_or_config from nautobot.apps.ui import NavMenuAddButton, NavMenuGroup, NavMenuItem, NavMenuTab -if get_app_settings_or_config("nautobot_chatops", "enable_grafana"): - from .integrations.grafana.navigation import items as grafana_items -else: - grafana_items = () - +from nautobot_chatops.integrations.grafana.navigation import items as grafana_items items = [ NavMenuItem( diff --git a/nautobot_chatops/tables.py b/nautobot_chatops/tables.py index efdcd0fa..395eea49 100644 --- a/nautobot_chatops/tables.py +++ b/nautobot_chatops/tables.py @@ -3,6 +3,13 @@ from django_tables2 import LinkColumn, TemplateColumn from nautobot.core.tables import BaseTable, ButtonsColumn, ToggleColumn +# pylint: disable=W0611 +from nautobot_chatops.integrations.grafana.tables import ( # noqa: F401 unused-import these imports are required for list views to work + GrafanaDashboardTable, + GrafanaPanelTable, + GrafanaPanelVariableTable, +) + from .models import AccessGrant, ChatOpsAccountLink, CommandLog, CommandToken diff --git a/nautobot_chatops/templates/nautobot_chatops_grafana/grafana_disabled.html b/nautobot_chatops/templates/nautobot_chatops_grafana/grafana_disabled.html new file mode 100644 index 00000000..519ac0ae --- /dev/null +++ b/nautobot_chatops/templates/nautobot_chatops_grafana/grafana_disabled.html @@ -0,0 +1,16 @@ +{% extends 'base.html' %} + +{% block content %} +
+
+
+ +
+
+
+{% endblock content %} diff --git a/nautobot_chatops/urls.py b/nautobot_chatops/urls.py index a3e5a559..531a0075 100644 --- a/nautobot_chatops/urls.py +++ b/nautobot_chatops/urls.py @@ -1,13 +1,11 @@ """Django urlpatterns declaration for nautobot_chatops app.""" -import logging - from django.templatetags.static import static from django.urls import path from django.views.generic import RedirectView -from nautobot.apps.config import get_app_settings_or_config from nautobot.extras.views import ObjectChangeLogView, ObjectNotesView +from nautobot_chatops.integrations.grafana.urls import urlpatterns as grafana_urlpatterns from nautobot_chatops.models import AccessGrant, ChatOpsAccountLink, CommandLog, CommandToken from nautobot_chatops.views import ( AccessGrantBulkDeleteView, @@ -25,19 +23,6 @@ CommandTokenView, ) -if get_app_settings_or_config("nautobot_chatops", "enable_grafana"): - try: - from nautobot_chatops.integrations.grafana.urls import urlpatterns as grafana_urlpatterns - # pylint: disable-next=broad-except - except Exception: - grafana_urlpatterns = [] - logger = logging.getLogger(__name__) - logger.warning("Grafana ChatOps integration is not available.", exc_info=True) -else: - grafana_urlpatterns = [] - logger = logging.getLogger(__name__) - logger.warning("Grafana ChatOps integration is not available.", exc_info=True) - urlpatterns = [ path("", CommandLogListView.as_view(), name="commandlog_list"), path( diff --git a/nautobot_chatops/views.py b/nautobot_chatops/views.py index 0bd8d947..8bbee037 100644 --- a/nautobot_chatops/views.py +++ b/nautobot_chatops/views.py @@ -5,7 +5,10 @@ """ from django.contrib.auth.mixins import PermissionRequiredMixin +from django.core.exceptions import ImproperlyConfigured +from django.http import Http404 from django.shortcuts import render +from nautobot.apps.config import get_app_settings_or_config from nautobot.core.forms import restrict_form_fields from nautobot.core.utils.requests import normalize_querydict from nautobot.core.views.generic import BulkDeleteView, ObjectDeleteView, ObjectEditView, ObjectListView, ObjectView @@ -21,6 +24,22 @@ from nautobot_chatops.tables import AccessGrantTable, ChatOpsAccountLinkTable, CommandLogTable, CommandTokenTable +class SettingsControlledViewMixin: + """View mixin to enable or disable views based on constance settings.""" + + enable_view_setting = None + + def dispatch(self, request, *args, **kwargs): + """Return a 404 if the view is not enabled in the settings.""" + if not getattr(self, "enable_view_setting", None): + raise ImproperlyConfigured( + "Property `enable_view_setting` must be defined on the view to use SettingsControlledView." + ) + if not get_app_settings_or_config("nautobot_chatops", self.enable_view_setting): + raise Http404 + return super().dispatch(request, *args, **kwargs) + + class CommandLogListView(PermissionRequiredMixin, ObjectListView): """View for listing all extant Command Logs.""" diff --git a/nautobot_chatops/workers/__init__.py b/nautobot_chatops/workers/__init__.py index d38ec373..8169abcd 100644 --- a/nautobot_chatops/workers/__init__.py +++ b/nautobot_chatops/workers/__init__.py @@ -18,7 +18,6 @@ from nautobot.extras.context_managers import web_request_context from nautobot_chatops.choices import AccessGrantTypeChoices, CommandStatusChoices -from nautobot_chatops.integrations.utils import ALL_INTEGRATIONS, DISABLED_INTEGRATIONS from nautobot_chatops.metrics import command_histogram, request_command_cntr from nautobot_chatops.models import AccessGrant from nautobot_chatops.utils import create_command_log @@ -58,6 +57,11 @@ def get_commands_registry(): """Populate and return the _commands_registry dictionary with all known commands, subcommands, and workers.""" + from nautobot_chatops.integrations.utils import ( # pylint: disable=import-outside-toplevel + ALL_INTEGRATIONS, + DISABLED_INTEGRATIONS, + ) + global _commands_registry # pylint: disable=global-variable-not-assigned global _registry_initialized # pylint: disable=global-statement if _registry_initialized: