diff --git a/dashboard/openstack_utils/connect.py b/dashboard/openstack_utils/connect.py index 5647e74..798ad54 100644 --- a/dashboard/openstack_utils/connect.py +++ b/dashboard/openstack_utils/connect.py @@ -3,6 +3,21 @@ from openstack.connection import Connection from watcher_visio.settings import OPENSTACK_CLOUD, OPENSTACK_REGION_NAME + +def check_openstack() -> dict: + """ + Lightweight check that OpenStack is reachable (connection only). + Returns {"status": "ok"} or {"status": "error", "message": "..."}. + """ + try: + conn = openstack.connect(cloud=OPENSTACK_CLOUD, region_name=OPENSTACK_REGION_NAME) + if conn is None: + return {"status": "error", "message": "No connection"} + return {"status": "ok"} + except Exception as e: + return {"status": "error", "message": str(e) or "Connection failed"} + + def get_connection() -> Connection: connection = openstack.connect(cloud=OPENSTACK_CLOUD, region_name=OPENSTACK_REGION_NAME) return connection diff --git a/dashboard/prometheus_utils/query.py b/dashboard/prometheus_utils/query.py index a8e0467..b1a4a2c 100644 --- a/dashboard/prometheus_utils/query.py +++ b/dashboard/prometheus_utils/query.py @@ -2,6 +2,29 @@ import requests from watcher_visio.settings import PROMETHEUS_URL +# Timeout for lightweight health check (seconds) +CHECK_TIMEOUT = 5 + + +def check_prometheus() -> dict: + """ + Lightweight check that Prometheus is reachable. + Returns {"status": "ok"} or {"status": "error", "message": "..."}. + """ + url = f"{PROMETHEUS_URL.rstrip('/')}/api/v1/query" + try: + response = requests.get(url, params={"query": "1"}, timeout=CHECK_TIMEOUT) + response.raise_for_status() + data = response.json() + if "data" in data and "result" in data["data"]: + return {"status": "ok"} + return {"status": "error", "message": "Invalid response"} + except requests.RequestException as e: + return {"status": "error", "message": str(e) or "Connection failed"} + except (ValueError, KeyError) as e: + return {"status": "error", "message": str(e) or "Invalid response"} + + def query_prometheus(query: str) -> str | list[str]: url = f"{PROMETHEUS_URL}/api/v1/query" params = { diff --git a/dashboard/tests/test_views.py b/dashboard/tests/test_views.py index 0682277..30335dc 100644 --- a/dashboard/tests/test_views.py +++ b/dashboard/tests/test_views.py @@ -12,6 +12,7 @@ from dashboard.views import ( collect_audits, api_stats, api_audits, + api_source_status, ) @@ -259,3 +260,102 @@ class ApiAuditsTest(TestCase): data = json.loads(response.content) self.assertEqual(data["audits"][0]["name"], "Cached Audit") self.assertEqual(data["current_cluster"], cached_cluster) + + +class ApiSourceStatusTest(TestCase): + """Tests for api_source_status view.""" + + def setUp(self): + self.factory = RequestFactory() + + @patch("dashboard.views.settings") + def test_api_source_status_mock_returns_mock_status(self, mock_settings): + mock_settings.USE_MOCK_DATA = True + mock_settings.SOURCE_STATUS_CACHE_TTL = 30 + request = self.factory.get("/api/source-status/") + response = api_source_status(request) + self.assertEqual(response.status_code, 200) + self.assertEqual(response["Content-Type"], "application/json") + data = json.loads(response.content) + self.assertEqual(data["prometheus"]["status"], "mock") + self.assertEqual(data["openstack"]["status"], "mock") + + @patch("dashboard.views.check_openstack") + @patch("dashboard.views.check_prometheus") + @patch("dashboard.views.settings") + def test_api_source_status_both_ok_returns_ok( + self, mock_settings, mock_check_prometheus, mock_check_openstack + ): + mock_settings.USE_MOCK_DATA = False + mock_settings.SOURCE_STATUS_CACHE_TTL = 30 + mock_check_prometheus.return_value = {"status": "ok"} + mock_check_openstack.return_value = {"status": "ok"} + cache.clear() + request = self.factory.get("/api/source-status/") + response = api_source_status(request) + self.assertEqual(response.status_code, 200) + data = json.loads(response.content) + self.assertEqual(data["prometheus"]["status"], "ok") + self.assertEqual(data["openstack"]["status"], "ok") + mock_check_prometheus.assert_called_once() + mock_check_openstack.assert_called_once() + + @patch("dashboard.views.check_openstack") + @patch("dashboard.views.check_prometheus") + @patch("dashboard.views.settings") + def test_api_source_status_prometheus_error_returns_error_message( + self, mock_settings, mock_check_prometheus, mock_check_openstack + ): + mock_settings.USE_MOCK_DATA = False + mock_settings.SOURCE_STATUS_CACHE_TTL = 30 + mock_check_prometheus.return_value = {"status": "error", "message": "Connection refused"} + mock_check_openstack.return_value = {"status": "ok"} + cache.clear() + request = self.factory.get("/api/source-status/") + response = api_source_status(request) + self.assertEqual(response.status_code, 200) + data = json.loads(response.content) + self.assertEqual(data["prometheus"]["status"], "error") + self.assertEqual(data["prometheus"]["message"], "Connection refused") + self.assertEqual(data["openstack"]["status"], "ok") + + @patch("dashboard.views.check_openstack") + @patch("dashboard.views.check_prometheus") + @patch("dashboard.views.settings") + def test_api_source_status_openstack_error_returns_error_message( + self, mock_settings, mock_check_prometheus, mock_check_openstack + ): + mock_settings.USE_MOCK_DATA = False + mock_settings.SOURCE_STATUS_CACHE_TTL = 30 + mock_check_prometheus.return_value = {"status": "ok"} + mock_check_openstack.return_value = {"status": "error", "message": "Auth failed"} + cache.clear() + request = self.factory.get("/api/source-status/") + response = api_source_status(request) + self.assertEqual(response.status_code, 200) + data = json.loads(response.content) + self.assertEqual(data["prometheus"]["status"], "ok") + self.assertEqual(data["openstack"]["status"], "error") + self.assertEqual(data["openstack"]["message"], "Auth failed") + + @patch("dashboard.views.check_openstack") + @patch("dashboard.views.check_prometheus") + @patch("dashboard.views.settings") + def test_api_source_status_uses_cache( + self, mock_settings, mock_check_prometheus, mock_check_openstack + ): + mock_settings.USE_MOCK_DATA = False + mock_settings.SOURCE_STATUS_CACHE_TTL = 30 + cache.clear() + cached = { + "prometheus": {"status": "ok"}, + "openstack": {"status": "ok"}, + } + cache.set("dashboard_source_status", cached, timeout=30) + request = self.factory.get("/api/source-status/") + response = api_source_status(request) + mock_check_prometheus.assert_not_called() + mock_check_openstack.assert_not_called() + data = json.loads(response.content) + self.assertEqual(data["prometheus"]["status"], "ok") + self.assertEqual(data["openstack"]["status"], "ok") diff --git a/dashboard/urls.py b/dashboard/urls.py index cc35b0a..0121c33 100644 --- a/dashboard/urls.py +++ b/dashboard/urls.py @@ -5,4 +5,5 @@ urlpatterns = [ path('', views.index, name='index'), path('api/stats/', views.api_stats), path('api/audits/', views.api_audits), + path('api/source-status/', views.api_source_status), ] \ No newline at end of file diff --git a/dashboard/views.py b/dashboard/views.py index c6762b6..3a86dc8 100644 --- a/dashboard/views.py +++ b/dashboard/views.py @@ -5,9 +5,9 @@ from django.conf import settings from django.core.cache import cache from django.http import JsonResponse from django.shortcuts import render -from dashboard.openstack_utils.connect import get_connection +from dashboard.openstack_utils.connect import check_openstack, get_connection from dashboard.openstack_utils.flavor import get_flavor_list -from dashboard.prometheus_utils.query import query_prometheus +from dashboard.prometheus_utils.query import check_prometheus, query_prometheus from dashboard.openstack_utils.audits import get_audits, get_current_cluster_cpu from dashboard.mock_data import get_mock_context @@ -264,4 +264,24 @@ def api_audits(request): connection = get_connection() current_cluster = get_current_cluster_cpu(connection) cache.set(cache_key_cluster, current_cluster, timeout=cache_ttl) - return JsonResponse({"audits": audits, "current_cluster": current_cluster}) \ No newline at end of file + return JsonResponse({"audits": audits, "current_cluster": current_cluster}) + + +def api_source_status(request): + """Return status of Prometheus and OpenStack data sources (ok / error / mock).""" + if getattr(settings, "USE_MOCK_DATA", False): + return JsonResponse({ + "prometheus": {"status": "mock"}, + "openstack": {"status": "mock"}, + }) + + cache_key = "dashboard_source_status" + cache_ttl = getattr(settings, "SOURCE_STATUS_CACHE_TTL", 30) + data = cache.get(cache_key) + if data is None: + data = { + "prometheus": check_prometheus(), + "openstack": check_openstack(), + } + cache.set(cache_key, data, timeout=cache_ttl) + return JsonResponse(data) \ No newline at end of file diff --git a/templates/base.html b/templates/base.html index b5b6dde..02537e4 100644 --- a/templates/base.html +++ b/templates/base.html @@ -29,6 +29,10 @@ Save as PDF {{ region.name }} +
+ Prometheus: … + OpenStack: … +