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 %}
+
+
+
+
+
Grafana Integration Disabled
+
+ The Grafana integration is currently disabled. Please contact your administrator for more information.
+
+
+
+
+
+{% 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: