- Standardized string quotes across multiple files to use double quotes for consistency. - Improved formatting of JSON dumps in mock data for better readability. - Enhanced the structure of various functions and data definitions for clarity. - Updated test cases to reflect changes in data structure and ensure accuracy.
This commit is contained in:
@@ -1,4 +1,5 @@
|
|||||||
"""Mock context for dashboard when USE_MOCK_DATA is enabled (no OpenStack/Prometheus)."""
|
"""Mock context for dashboard when USE_MOCK_DATA is enabled (no OpenStack/Prometheus)."""
|
||||||
|
|
||||||
import json
|
import json
|
||||||
|
|
||||||
|
|
||||||
@@ -35,15 +36,17 @@ def get_mock_context():
|
|||||||
"scope": "Full Cluster",
|
"scope": "Full Cluster",
|
||||||
"cpu_weight": "1.0",
|
"cpu_weight": "1.0",
|
||||||
"ram_weight": "1.0",
|
"ram_weight": "1.0",
|
||||||
"migrations": json.dumps([
|
"migrations": json.dumps(
|
||||||
{
|
[
|
||||||
"instanceName": "instance-1",
|
{
|
||||||
"source": "compute-0",
|
"instanceName": "instance-1",
|
||||||
"destination": "compute-3",
|
"source": "compute-0",
|
||||||
"flavor": "m1.small",
|
"destination": "compute-3",
|
||||||
"impact": "Low",
|
"flavor": "m1.small",
|
||||||
}
|
"impact": "Low",
|
||||||
]),
|
}
|
||||||
|
]
|
||||||
|
),
|
||||||
"host_labels": json.dumps(host_labels),
|
"host_labels": json.dumps(host_labels),
|
||||||
"cpu_current": json.dumps(cpu_current),
|
"cpu_current": json.dumps(cpu_current),
|
||||||
"cpu_projected": json.dumps(cpu_projected),
|
"cpu_projected": json.dumps(cpu_projected),
|
||||||
|
|||||||
@@ -1,13 +1,12 @@
|
|||||||
import pandas
|
|
||||||
|
|
||||||
from copy import copy
|
from copy import copy
|
||||||
|
|
||||||
|
import pandas
|
||||||
from openstack.connection import Connection
|
from openstack.connection import Connection
|
||||||
|
from watcher_visio.settings import PROMETHEUS_METRICS, WATCHER_ENDPOINT_NAME, WATCHER_INTERFACE_NAME
|
||||||
from watcher_visio.settings import WATCHER_ENDPOINT_NAME, WATCHER_INTERFACE_NAME, PROMETHEUS_METRICS
|
|
||||||
|
|
||||||
from dashboard.prometheus_utils.query import query_prometheus
|
from dashboard.prometheus_utils.query import query_prometheus
|
||||||
|
|
||||||
|
|
||||||
def convert_cpu_data(data: list):
|
def convert_cpu_data(data: list):
|
||||||
metrics = []
|
metrics = []
|
||||||
|
|
||||||
@@ -16,33 +15,37 @@ def convert_cpu_data(data: list):
|
|||||||
|
|
||||||
for entry in data:
|
for entry in data:
|
||||||
for t, val in entry["values"]:
|
for t, val in entry["values"]:
|
||||||
metrics.append({
|
metrics.append(
|
||||||
"timestamp": int(t),
|
{
|
||||||
"host": entry["metric"]["host"],
|
"timestamp": int(t),
|
||||||
"cpu_usage": float(val),
|
"host": entry["metric"]["host"],
|
||||||
"instance": entry["metric"]["instanceName"]
|
"cpu_usage": float(val),
|
||||||
})
|
"instance": entry["metric"]["instanceName"],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
df_cpu = pandas.DataFrame(metrics)
|
df_cpu = pandas.DataFrame(metrics)
|
||||||
df_cpu["timestamp"] = pandas.to_datetime(df_cpu["timestamp"], unit="s")
|
df_cpu["timestamp"] = pandas.to_datetime(df_cpu["timestamp"], unit="s")
|
||||||
|
|
||||||
# Aggregate CPU usage per host
|
# Aggregate CPU usage per host
|
||||||
return (
|
return (
|
||||||
df_cpu.groupby(["host", "timestamp"])["cpu_usage"].sum()
|
df_cpu.groupby(["host", "timestamp"])["cpu_usage"]
|
||||||
.groupby("host").mean()
|
.sum()
|
||||||
|
.groupby("host")
|
||||||
|
.mean()
|
||||||
.reset_index()
|
.reset_index()
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def get_current_cluster_cpu(connection: Connection) -> dict:
|
def get_current_cluster_cpu(connection: Connection) -> dict:
|
||||||
"""Return current per-host CPU state for the cluster (no Watcher dependency)."""
|
"""Return current per-host CPU state for the cluster (no Watcher dependency)."""
|
||||||
cpu_data = query_prometheus(PROMETHEUS_METRICS['cpu_usage'])
|
cpu_data = query_prometheus(PROMETHEUS_METRICS["cpu_usage"])
|
||||||
cpu_metrics = convert_cpu_data(data=cpu_data)
|
cpu_metrics = convert_cpu_data(data=cpu_data)
|
||||||
if cpu_metrics.empty:
|
if cpu_metrics.empty:
|
||||||
return {"host_labels": [], "cpu_current": []}
|
return {"host_labels": [], "cpu_current": []}
|
||||||
return {
|
return {
|
||||||
"host_labels": cpu_metrics['host'].to_list(),
|
"host_labels": cpu_metrics["host"].to_list(),
|
||||||
"cpu_current": cpu_metrics['cpu_usage'].to_list(),
|
"cpu_current": cpu_metrics["cpu_usage"].to_list(),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -50,43 +53,38 @@ def get_audits(connection: Connection) -> list[dict] | None:
|
|||||||
session = connection.session
|
session = connection.session
|
||||||
|
|
||||||
watcher_endpoint = connection.endpoint_for(
|
watcher_endpoint = connection.endpoint_for(
|
||||||
service_type=WATCHER_ENDPOINT_NAME,
|
service_type=WATCHER_ENDPOINT_NAME, interface=WATCHER_INTERFACE_NAME
|
||||||
interface=WATCHER_INTERFACE_NAME
|
|
||||||
)
|
)
|
||||||
|
|
||||||
# Collect instances prometheus metrics
|
# Collect instances prometheus metrics
|
||||||
cpu_data = query_prometheus(PROMETHEUS_METRICS['cpu_usage'])
|
cpu_data = query_prometheus(PROMETHEUS_METRICS["cpu_usage"])
|
||||||
|
|
||||||
cpu_metrics = convert_cpu_data(data=cpu_data)
|
cpu_metrics = convert_cpu_data(data=cpu_data)
|
||||||
|
|
||||||
# Fetch audit list
|
# Fetch audit list
|
||||||
audits_resp = session.get(
|
audits_resp = session.get(f"{watcher_endpoint}/v1/audits")
|
||||||
f"{watcher_endpoint}/v1/audits"
|
|
||||||
)
|
|
||||||
audits_resp.raise_for_status()
|
audits_resp.raise_for_status()
|
||||||
audits_resp = audits_resp.json().get('audits') or []
|
audits_resp = audits_resp.json().get("audits") or []
|
||||||
|
|
||||||
# Fetch action plan list
|
# Fetch action plan list
|
||||||
actionplans_resp = session.get(
|
actionplans_resp = session.get(f"{watcher_endpoint}/v1/action_plans")
|
||||||
f"{watcher_endpoint}/v1/action_plans"
|
|
||||||
)
|
|
||||||
actionplans_resp.raise_for_status()
|
actionplans_resp.raise_for_status()
|
||||||
actionplans_resp = actionplans_resp.json().get('action_plans') or []
|
actionplans_resp = actionplans_resp.json().get("action_plans") or []
|
||||||
|
|
||||||
# Filtering audits by PENDING state
|
# Filtering audits by PENDING state
|
||||||
pending_audits = [plan for plan in actionplans_resp if plan['state'] == "RECOMMENDED"]
|
pending_audits = [plan for plan in actionplans_resp if plan["state"] == "RECOMMENDED"]
|
||||||
|
|
||||||
result = []
|
result = []
|
||||||
for item in pending_audits:
|
for item in pending_audits:
|
||||||
projected_cpu_data = copy(cpu_data)
|
projected_cpu_data = copy(cpu_data)
|
||||||
|
|
||||||
audit_resp = session.get(
|
audit_resp = session.get(f"{watcher_endpoint}/v1/audits/{item['audit_uuid']}")
|
||||||
f"{watcher_endpoint}/v1/audits/{item['audit_uuid']}"
|
|
||||||
)
|
|
||||||
audit_resp.raise_for_status()
|
audit_resp.raise_for_status()
|
||||||
audit_resp = audit_resp.json()
|
audit_resp = audit_resp.json()
|
||||||
|
|
||||||
actionplan = next(filter(lambda x: x.get("audit_uuid") == audit_resp['uuid'], actionplans_resp), None)
|
actionplan = next(
|
||||||
|
filter(lambda x: x.get("audit_uuid") == audit_resp["uuid"], actionplans_resp), None
|
||||||
|
)
|
||||||
if actionplan is None:
|
if actionplan is None:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
@@ -94,49 +92,55 @@ def get_audits(connection: Connection) -> list[dict] | None:
|
|||||||
f"{watcher_endpoint}/v1/actions/?action_plan_uuid={actionplan['uuid']}"
|
f"{watcher_endpoint}/v1/actions/?action_plan_uuid={actionplan['uuid']}"
|
||||||
)
|
)
|
||||||
actions_resp.raise_for_status()
|
actions_resp.raise_for_status()
|
||||||
actions_resp = actions_resp.json().get('actions') or []
|
actions_resp = actions_resp.json().get("actions") or []
|
||||||
|
|
||||||
migrations = []
|
migrations = []
|
||||||
mapping = {}
|
mapping = {}
|
||||||
for action in actions_resp:
|
for action in actions_resp:
|
||||||
action_resp = session.get(
|
action_resp = session.get(f"{watcher_endpoint}/v1/actions/{action['uuid']}")
|
||||||
f"{watcher_endpoint}/v1/actions/{action['uuid']}"
|
|
||||||
)
|
|
||||||
action_resp.raise_for_status()
|
action_resp.raise_for_status()
|
||||||
action_resp = action_resp.json()
|
action_resp = action_resp.json()
|
||||||
|
|
||||||
server = connection.get_server_by_id(action_resp['input_parameters']['resource_id'])
|
server = connection.get_server_by_id(action_resp["input_parameters"]["resource_id"])
|
||||||
params = action_resp['input_parameters']
|
params = action_resp["input_parameters"]
|
||||||
mapping[params['resource_name']] = params['destination_node']
|
mapping[params["resource_name"]] = params["destination_node"]
|
||||||
|
|
||||||
migrations.append({
|
migrations.append(
|
||||||
"instanceName": action_resp['input_parameters']['resource_name'],
|
{
|
||||||
"source": action_resp['input_parameters']['source_node'],
|
"instanceName": action_resp["input_parameters"]["resource_name"],
|
||||||
"destination": action_resp['input_parameters']['destination_node'],
|
"source": action_resp["input_parameters"]["source_node"],
|
||||||
"flavor": server.flavor.name,
|
"destination": action_resp["input_parameters"]["destination_node"],
|
||||||
"impact": 'Low'
|
"flavor": server.flavor.name,
|
||||||
})
|
"impact": "Low",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
for entry in projected_cpu_data:
|
for entry in projected_cpu_data:
|
||||||
if (instance := entry['metric']['instanceName']) in mapping:
|
if (instance := entry["metric"]["instanceName"]) in mapping:
|
||||||
entry['metric']['host'] = mapping[instance]
|
entry["metric"]["host"] = mapping[instance]
|
||||||
|
|
||||||
projected_cpu_metrics = convert_cpu_data(projected_cpu_data)
|
projected_cpu_metrics = convert_cpu_data(projected_cpu_data)
|
||||||
|
|
||||||
result.append({
|
result.append(
|
||||||
"id": audit_resp['uuid'],
|
{
|
||||||
"name": audit_resp['name'],
|
"id": audit_resp["uuid"],
|
||||||
"created_at": audit_resp['created_at'],
|
"name": audit_resp["name"],
|
||||||
"strategy": audit_resp['strategy_name'],
|
"created_at": audit_resp["created_at"],
|
||||||
"goal": audit_resp['goal_name'],
|
"strategy": audit_resp["strategy_name"],
|
||||||
"type": audit_resp['audit_type'],
|
"goal": audit_resp["goal_name"],
|
||||||
"scope": audit_resp['scope'],
|
"type": audit_resp["audit_type"],
|
||||||
"cpu_weight": audit_resp['parameters'].get('weights', {}).get('instance_cpu_usage_weight', "none"),
|
"scope": audit_resp["scope"],
|
||||||
"ram_weight": audit_resp['parameters'].get('weights', {}).get('instance_ram_usage_weight', "none"),
|
"cpu_weight": audit_resp["parameters"]
|
||||||
"migrations": migrations,
|
.get("weights", {})
|
||||||
"host_labels": cpu_metrics['host'].to_list(),
|
.get("instance_cpu_usage_weight", "none"),
|
||||||
"cpu_current": cpu_metrics['cpu_usage'].to_list(),
|
"ram_weight": audit_resp["parameters"]
|
||||||
"cpu_projected": projected_cpu_metrics['cpu_usage'].to_list(),
|
.get("weights", {})
|
||||||
})
|
.get("instance_ram_usage_weight", "none"),
|
||||||
|
"migrations": migrations,
|
||||||
|
"host_labels": cpu_metrics["host"].to_list(),
|
||||||
|
"cpu_current": cpu_metrics["cpu_usage"].to_list(),
|
||||||
|
"cpu_projected": projected_cpu_metrics["cpu_usage"].to_list(),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
return result
|
return result
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import openstack
|
import openstack
|
||||||
from openstack.connection import Connection
|
from openstack.connection import Connection
|
||||||
|
|
||||||
from watcher_visio.settings import OPENSTACK_CLOUD, OPENSTACK_REGION_NAME
|
from watcher_visio.settings import OPENSTACK_CLOUD, OPENSTACK_REGION_NAME
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
|
from collections import Counter
|
||||||
|
|
||||||
from openstack.connection import Connection
|
from openstack.connection import Connection
|
||||||
|
|
||||||
from collections import Counter
|
|
||||||
|
|
||||||
def get_flavor_list(connection: Connection) -> dict:
|
def get_flavor_list(connection: Connection) -> dict:
|
||||||
servers = list(connection.compute.servers(all_projects=True))
|
servers = list(connection.compute.servers(all_projects=True))
|
||||||
flavor_ids = [s.flavor['id'] for s in servers if 'id' in s.flavor]
|
flavor_ids = [s.flavor["id"] for s in servers if "id" in s.flavor]
|
||||||
flavor_count = Counter(flavor_ids).most_common()
|
flavor_count = Counter(flavor_ids).most_common()
|
||||||
|
|
||||||
flavors = list(flavor_count)
|
flavors = list(flavor_count)
|
||||||
@@ -13,10 +14,7 @@ def get_flavor_list(connection: Connection) -> dict:
|
|||||||
placeholder = {"name": "—", "count": 0}
|
placeholder = {"name": "—", "count": 0}
|
||||||
for idx, prefix in [(0, "first"), (1, "second"), (2, "third")]:
|
for idx, prefix in [(0, "first"), (1, "second"), (2, "third")]:
|
||||||
if len(flavors) > idx:
|
if len(flavors) > idx:
|
||||||
result[f"{prefix}_common_flavor"] = {
|
result[f"{prefix}_common_flavor"] = {"name": flavors[idx][0], "count": flavors[idx][1]}
|
||||||
"name": flavors[idx][0],
|
|
||||||
"count": flavors[idx][1]
|
|
||||||
}
|
|
||||||
else:
|
else:
|
||||||
result[f"{prefix}_common_flavor"] = placeholder
|
result[f"{prefix}_common_flavor"] = placeholder
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import requests
|
import requests
|
||||||
|
|
||||||
from watcher_visio.settings import PROMETHEUS_URL
|
from watcher_visio.settings import PROMETHEUS_URL
|
||||||
|
|
||||||
# Timeout for lightweight health check (seconds)
|
# Timeout for lightweight health check (seconds)
|
||||||
|
|||||||
@@ -2,29 +2,33 @@ from django import template
|
|||||||
|
|
||||||
register = template.Library()
|
register = template.Library()
|
||||||
|
|
||||||
|
|
||||||
@register.filter
|
@register.filter
|
||||||
def div(a, b):
|
def div(a, b):
|
||||||
try:
|
try:
|
||||||
return float(a) / float(b)
|
return float(a) / float(b)
|
||||||
except:
|
except (TypeError, ValueError, ZeroDivisionError):
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
|
|
||||||
@register.filter
|
@register.filter
|
||||||
def mul(a, b):
|
def mul(a, b):
|
||||||
try:
|
try:
|
||||||
return float(a) * float(b)
|
return float(a) * float(b)
|
||||||
except:
|
except (TypeError, ValueError):
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
|
|
||||||
@register.filter
|
@register.filter
|
||||||
def sub(a, b):
|
def sub(a, b):
|
||||||
try:
|
try:
|
||||||
return float(a) - float(b)
|
return float(a) - float(b)
|
||||||
except:
|
except (TypeError, ValueError):
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
|
|
||||||
@register.filter
|
@register.filter
|
||||||
def convert_bytes(bytes_value, target_unit='GB'):
|
def convert_bytes(bytes_value, target_unit="GB"):
|
||||||
"""
|
"""
|
||||||
Convert bytes to specific unit
|
Convert bytes to specific unit
|
||||||
|
|
||||||
@@ -41,16 +45,16 @@ def convert_bytes(bytes_value, target_unit='GB'):
|
|||||||
except (ValueError, TypeError):
|
except (ValueError, TypeError):
|
||||||
return 0.0
|
return 0.0
|
||||||
conversion_factors = {
|
conversion_factors = {
|
||||||
'B': 1,
|
"B": 1,
|
||||||
'KB': 1024,
|
"KB": 1024,
|
||||||
'MB': 1024 * 1024,
|
"MB": 1024 * 1024,
|
||||||
'GB': 1024 * 1024 * 1024,
|
"GB": 1024 * 1024 * 1024,
|
||||||
'TB': 1024 * 1024 * 1024 * 1024,
|
"TB": 1024 * 1024 * 1024 * 1024,
|
||||||
}
|
}
|
||||||
|
|
||||||
target_unit = target_unit.upper()
|
target_unit = target_unit.upper()
|
||||||
if target_unit not in conversion_factors:
|
if target_unit not in conversion_factors:
|
||||||
target_unit = 'MB'
|
target_unit = "MB"
|
||||||
|
|
||||||
result = bytes_value / conversion_factors[target_unit]
|
result = bytes_value / conversion_factors[target_unit]
|
||||||
return round(result, 1)
|
return round(result, 1)
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
"""Tests for dashboard.openstack_utils.audits."""
|
"""Tests for dashboard.openstack_utils.audits."""
|
||||||
|
|
||||||
from unittest.mock import MagicMock, patch
|
from unittest.mock import MagicMock, patch
|
||||||
|
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
@@ -31,8 +32,10 @@ class ConvertCpuDataTest(TestCase):
|
|||||||
self.assertEqual(len(hosts), 2)
|
self.assertEqual(len(hosts), 2)
|
||||||
self.assertIn("compute-0", hosts)
|
self.assertIn("compute-0", hosts)
|
||||||
self.assertIn("compute-1", hosts)
|
self.assertIn("compute-1", hosts)
|
||||||
# compute-0: (10+20)/2 for ts 1000 and 5 for ts 1000 -> groupby host,timestamp sum -> then groupby host mean
|
# compute-0: (10+20)/2 for ts 1000 and 5 for ts 1000 -> groupby host,timestamp sum
|
||||||
# For compute-0: two timestamps 1000 (10+5=15) and 1001 (20). Mean over timestamps = (15+20)/2 = 17.5
|
# -> then groupby host mean
|
||||||
|
# For compute-0: two timestamps 1000 (10+5=15) and 1001 (20).
|
||||||
|
# Mean over timestamps = (15+20)/2 = 17.5
|
||||||
# For compute-1: one value 30
|
# For compute-1: one value 30
|
||||||
by_host = result.set_index("host")["cpu_usage"]
|
by_host = result.set_index("host")["cpu_usage"]
|
||||||
self.assertAlmostEqual(by_host["compute-0"], 17.5)
|
self.assertAlmostEqual(by_host["compute-0"], 17.5)
|
||||||
@@ -60,11 +63,14 @@ class GetCurrentClusterCpuTest(TestCase):
|
|||||||
@patch("dashboard.openstack_utils.audits.query_prometheus")
|
@patch("dashboard.openstack_utils.audits.query_prometheus")
|
||||||
def test_returns_host_labels_and_cpu_current(self, mock_query, mock_convert):
|
def test_returns_host_labels_and_cpu_current(self, mock_query, mock_convert):
|
||||||
import pandas as pd
|
import pandas as pd
|
||||||
|
|
||||||
mock_query.return_value = [{"metric": {"host": "h0"}, "values": [[0, "1.0"]]}]
|
mock_query.return_value = [{"metric": {"host": "h0"}, "values": [[0, "1.0"]]}]
|
||||||
mock_convert.return_value = pd.DataFrame({
|
mock_convert.return_value = pd.DataFrame(
|
||||||
"host": ["compute-0", "compute-1"],
|
{
|
||||||
"cpu_usage": [25.0, 35.0],
|
"host": ["compute-0", "compute-1"],
|
||||||
})
|
"cpu_usage": [25.0, 35.0],
|
||||||
|
}
|
||||||
|
)
|
||||||
conn = MagicMock()
|
conn = MagicMock()
|
||||||
result = get_current_cluster_cpu(conn)
|
result = get_current_cluster_cpu(conn)
|
||||||
self.assertEqual(result["host_labels"], ["compute-0", "compute-1"])
|
self.assertEqual(result["host_labels"], ["compute-0", "compute-1"])
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
"""Tests for dashboard.openstack_utils.flavor."""
|
"""Tests for dashboard.openstack_utils.flavor."""
|
||||||
|
|
||||||
from unittest.mock import MagicMock
|
from unittest.mock import MagicMock
|
||||||
|
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
"""Tests for dashboard.templatetags.mathfilters."""
|
"""Tests for dashboard.templatetags.mathfilters."""
|
||||||
from django.test import TestCase
|
|
||||||
from django.template import Template, Context
|
|
||||||
|
|
||||||
from dashboard.templatetags.mathfilters import div, mul, sub, convert_bytes
|
from django.template import Context, Template
|
||||||
|
from django.test import TestCase
|
||||||
|
|
||||||
|
from dashboard.templatetags.mathfilters import convert_bytes, div, mul, sub
|
||||||
|
|
||||||
|
|
||||||
class DivFilterTest(TestCase):
|
class DivFilterTest(TestCase):
|
||||||
@@ -48,25 +49,25 @@ class SubFilterTest(TestCase):
|
|||||||
class ConvertBytesFilterTest(TestCase):
|
class ConvertBytesFilterTest(TestCase):
|
||||||
"""Tests for the convert_bytes template filter."""
|
"""Tests for the convert_bytes template filter."""
|
||||||
|
|
||||||
def test_convert_to_B(self):
|
def test_convert_to_b(self):
|
||||||
self.assertEqual(convert_bytes(1024, "B"), 1024.0)
|
self.assertEqual(convert_bytes(1024, "B"), 1024.0)
|
||||||
|
|
||||||
def test_convert_to_KB(self):
|
def test_convert_to_kb(self):
|
||||||
self.assertEqual(convert_bytes(2048, "KB"), 2.0)
|
self.assertEqual(convert_bytes(2048, "KB"), 2.0)
|
||||||
|
|
||||||
def test_convert_to_MB(self):
|
def test_convert_to_mb(self):
|
||||||
self.assertEqual(convert_bytes(1024 * 1024 * 3, "MB"), 3.0)
|
self.assertEqual(convert_bytes(1024 * 1024 * 3, "MB"), 3.0)
|
||||||
|
|
||||||
def test_convert_to_GB(self):
|
def test_convert_to_gb(self):
|
||||||
self.assertEqual(convert_bytes(1024 ** 3 * 5, "GB"), 5.0)
|
self.assertEqual(convert_bytes(1024**3 * 5, "GB"), 5.0)
|
||||||
|
|
||||||
def test_convert_to_TB(self):
|
def test_convert_to_tb(self):
|
||||||
self.assertEqual(convert_bytes(1024 ** 4, "TB"), 1.0)
|
self.assertEqual(convert_bytes(1024**4, "TB"), 1.0)
|
||||||
|
|
||||||
def test_convert_default_GB(self):
|
def test_convert_default_gb(self):
|
||||||
self.assertEqual(convert_bytes(1024 ** 3 * 2), 2.0)
|
self.assertEqual(convert_bytes(1024**3 * 2), 2.0)
|
||||||
|
|
||||||
def test_convert_invalid_unit_fallback_to_MB(self):
|
def test_convert_invalid_unit_fallback_to_mb(self):
|
||||||
self.assertEqual(convert_bytes(1024 * 1024, "invalid"), 1.0)
|
self.assertEqual(convert_bytes(1024 * 1024, "invalid"), 1.0)
|
||||||
self.assertEqual(convert_bytes(1024 * 1024, "xyz"), 1.0)
|
self.assertEqual(convert_bytes(1024 * 1024, "xyz"), 1.0)
|
||||||
|
|
||||||
@@ -79,8 +80,8 @@ class ConvertBytesFilterTest(TestCase):
|
|||||||
self.assertEqual(convert_bytes(1536 * 1024 * 1024, "GB"), 1.5)
|
self.assertEqual(convert_bytes(1536 * 1024 * 1024, "GB"), 1.5)
|
||||||
|
|
||||||
def test_convert_case_insensitive_unit(self):
|
def test_convert_case_insensitive_unit(self):
|
||||||
self.assertEqual(convert_bytes(1024 ** 3, "gb"), 1.0)
|
self.assertEqual(convert_bytes(1024**3, "gb"), 1.0)
|
||||||
self.assertEqual(convert_bytes(1024 ** 3, "GB"), 1.0)
|
self.assertEqual(convert_bytes(1024**3, "GB"), 1.0)
|
||||||
|
|
||||||
|
|
||||||
class MathfiltersTemplateIntegrationTest(TestCase):
|
class MathfiltersTemplateIntegrationTest(TestCase):
|
||||||
@@ -100,4 +101,4 @@ class MathfiltersTemplateIntegrationTest(TestCase):
|
|||||||
|
|
||||||
def test_convert_bytes_in_template(self):
|
def test_convert_bytes_in_template(self):
|
||||||
t = Template("{% load mathfilters %}{{ bytes|convert_bytes:'GB' }}")
|
t = Template("{% load mathfilters %}{{ bytes|convert_bytes:'GB' }}")
|
||||||
self.assertEqual(t.render(Context({"bytes": 1024 ** 3 * 2})), "2.0")
|
self.assertEqual(t.render(Context({"bytes": 1024**3 * 2})), "2.0")
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
"""Tests for dashboard.mock_data."""
|
"""Tests for dashboard.mock_data."""
|
||||||
|
|
||||||
import json
|
import json
|
||||||
|
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
@@ -11,7 +12,17 @@ class GetMockContextTest(TestCase):
|
|||||||
|
|
||||||
def test_returns_all_top_level_keys(self):
|
def test_returns_all_top_level_keys(self):
|
||||||
ctx = get_mock_context()
|
ctx = get_mock_context()
|
||||||
expected_keys = {"region", "pcpu", "vcpu", "pram", "vram", "vm", "flavors", "audits", "current_cluster"}
|
expected_keys = {
|
||||||
|
"region",
|
||||||
|
"pcpu",
|
||||||
|
"vcpu",
|
||||||
|
"pram",
|
||||||
|
"vram",
|
||||||
|
"vm",
|
||||||
|
"flavors",
|
||||||
|
"audits",
|
||||||
|
"current_cluster",
|
||||||
|
}
|
||||||
self.assertEqual(set(ctx.keys()), expected_keys)
|
self.assertEqual(set(ctx.keys()), expected_keys)
|
||||||
|
|
||||||
def test_region_structure(self):
|
def test_region_structure(self):
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
"""Tests for dashboard.prometheus_utils.query."""
|
"""Tests for dashboard.prometheus_utils.query."""
|
||||||
from unittest.mock import patch, MagicMock
|
|
||||||
|
from unittest.mock import MagicMock, patch
|
||||||
|
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
|
|
||||||
@@ -12,13 +13,7 @@ class QueryPrometheusTest(TestCase):
|
|||||||
@patch("dashboard.prometheus_utils.query.requests.get")
|
@patch("dashboard.prometheus_utils.query.requests.get")
|
||||||
def test_single_result_returns_value_string(self, mock_get):
|
def test_single_result_returns_value_string(self, mock_get):
|
||||||
mock_response = MagicMock()
|
mock_response = MagicMock()
|
||||||
mock_response.json.return_value = {
|
mock_response.json.return_value = {"data": {"result": [{"value": ["1234567890", "42"]}]}}
|
||||||
"data": {
|
|
||||||
"result": [
|
|
||||||
{"value": ["1234567890", "42"]}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
mock_response.raise_for_status = MagicMock()
|
mock_response.raise_for_status = MagicMock()
|
||||||
mock_get.return_value = mock_response
|
mock_get.return_value = mock_response
|
||||||
|
|
||||||
|
|||||||
@@ -1,18 +1,17 @@
|
|||||||
"""Tests for dashboard.views."""
|
"""Tests for dashboard.views."""
|
||||||
import json
|
|
||||||
from unittest.mock import patch, MagicMock
|
|
||||||
|
|
||||||
from django.test import TestCase, RequestFactory
|
import json
|
||||||
|
from unittest.mock import MagicMock, patch
|
||||||
|
|
||||||
from django.core.cache import cache
|
from django.core.cache import cache
|
||||||
|
from django.test import RequestFactory, TestCase
|
||||||
|
|
||||||
from dashboard.views import (
|
from dashboard.views import (
|
||||||
index,
|
|
||||||
collect_context,
|
|
||||||
collect_stats,
|
|
||||||
collect_audits,
|
|
||||||
api_stats,
|
|
||||||
api_audits,
|
api_audits,
|
||||||
api_source_status,
|
api_source_status,
|
||||||
|
api_stats,
|
||||||
|
collect_context,
|
||||||
|
index,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -21,10 +20,31 @@ def _minimal_render_context(region_name="test", first_flavor_name="f1", vm_count
|
|||||||
return {
|
return {
|
||||||
"region": {"name": region_name, "hosts_total": 1},
|
"region": {"name": region_name, "hosts_total": 1},
|
||||||
"pcpu": {"total": 1, "usage": 0, "free": 1, "used_percentage": 0},
|
"pcpu": {"total": 1, "usage": 0, "free": 1, "used_percentage": 0},
|
||||||
"vcpu": {"total": 2, "allocated": 1, "free": 1, "allocated_percentage": 50, "overcommit_ratio": 1, "overcommit_max": 2},
|
"vcpu": {
|
||||||
|
"total": 2,
|
||||||
|
"allocated": 1,
|
||||||
|
"free": 1,
|
||||||
|
"allocated_percentage": 50,
|
||||||
|
"overcommit_ratio": 1,
|
||||||
|
"overcommit_max": 2,
|
||||||
|
},
|
||||||
"pram": {"total": 1024**3, "usage": 0, "free": 1024**3, "used_percentage": 0},
|
"pram": {"total": 1024**3, "usage": 0, "free": 1024**3, "used_percentage": 0},
|
||||||
"vram": {"total": 1024**3, "allocated": 0, "free": 1024**3, "allocated_percentage": 0, "overcommit_ratio": 0, "overcommit_max": 1},
|
"vram": {
|
||||||
"vm": {"count": vm_count, "active": vm_count, "stopped": 0, "avg_cpu": 1, "avg_ram": 0, "density": float(vm_count)},
|
"total": 1024**3,
|
||||||
|
"allocated": 0,
|
||||||
|
"free": 1024**3,
|
||||||
|
"allocated_percentage": 0,
|
||||||
|
"overcommit_ratio": 0,
|
||||||
|
"overcommit_max": 1,
|
||||||
|
},
|
||||||
|
"vm": {
|
||||||
|
"count": vm_count,
|
||||||
|
"active": vm_count,
|
||||||
|
"stopped": 0,
|
||||||
|
"avg_cpu": 1,
|
||||||
|
"avg_ram": 0,
|
||||||
|
"density": float(vm_count),
|
||||||
|
},
|
||||||
"flavors": {
|
"flavors": {
|
||||||
"first_common_flavor": {"name": first_flavor_name, "count": vm_count},
|
"first_common_flavor": {"name": first_flavor_name, "count": vm_count},
|
||||||
"second_common_flavor": {"name": "—", "count": 0},
|
"second_common_flavor": {"name": "—", "count": 0},
|
||||||
@@ -81,10 +101,18 @@ class CollectContextTest(TestCase):
|
|||||||
@patch("dashboard.views.get_flavor_list")
|
@patch("dashboard.views.get_flavor_list")
|
||||||
@patch("dashboard.views.get_connection")
|
@patch("dashboard.views.get_connection")
|
||||||
def test_collect_context_structure_and_calculation(
|
def test_collect_context_structure_and_calculation(
|
||||||
self, mock_get_connection, mock_get_flavor_list, mock_get_audits, mock_fetch_metrics, mock_get_current_cluster_cpu
|
self,
|
||||||
|
mock_get_connection,
|
||||||
|
mock_get_flavor_list,
|
||||||
|
mock_get_audits,
|
||||||
|
mock_fetch_metrics,
|
||||||
|
mock_get_current_cluster_cpu,
|
||||||
):
|
):
|
||||||
mock_get_connection.return_value = self._make_mock_connection("my-region")
|
mock_get_connection.return_value = self._make_mock_connection("my-region")
|
||||||
mock_get_current_cluster_cpu.return_value = {"host_labels": ["h0", "h1"], "cpu_current": [30.0, 40.0]}
|
mock_get_current_cluster_cpu.return_value = {
|
||||||
|
"host_labels": ["h0", "h1"],
|
||||||
|
"cpu_current": [30.0, 40.0],
|
||||||
|
}
|
||||||
mock_get_flavor_list.return_value = {
|
mock_get_flavor_list.return_value = {
|
||||||
"first_common_flavor": {"name": "m1.small", "count": 5},
|
"first_common_flavor": {"name": "m1.small", "count": 5},
|
||||||
"second_common_flavor": {"name": "—", "count": 0},
|
"second_common_flavor": {"name": "—", "count": 0},
|
||||||
@@ -125,6 +153,7 @@ class CollectContextTest(TestCase):
|
|||||||
self.assertEqual(len(context["audits"]), 1)
|
self.assertEqual(len(context["audits"]), 1)
|
||||||
# Serialized for JS
|
# Serialized for JS
|
||||||
import json
|
import json
|
||||||
|
|
||||||
self.assertIsInstance(context["audits"][0]["migrations"], str)
|
self.assertIsInstance(context["audits"][0]["migrations"], str)
|
||||||
self.assertEqual(json.loads(context["audits"][0]["host_labels"]), ["h0", "h1"])
|
self.assertEqual(json.loads(context["audits"][0]["host_labels"]), ["h0", "h1"])
|
||||||
self.assertIn("current_cluster", context)
|
self.assertIn("current_cluster", context)
|
||||||
@@ -187,7 +216,15 @@ class ApiStatsTest(TestCase):
|
|||||||
@patch("dashboard.views.settings")
|
@patch("dashboard.views.settings")
|
||||||
def test_api_stats_uses_cache(self, mock_settings, mock_collect_stats):
|
def test_api_stats_uses_cache(self, mock_settings, mock_collect_stats):
|
||||||
mock_settings.DASHBOARD_CACHE_TTL = 120
|
mock_settings.DASHBOARD_CACHE_TTL = 120
|
||||||
cached = {"region": {"name": "cached", "hosts_total": 1}, "pcpu": {}, "pram": {}, "vcpu": {}, "vram": {}, "vm": {}, "flavors": {}}
|
cached = {
|
||||||
|
"region": {"name": "cached", "hosts_total": 1},
|
||||||
|
"pcpu": {},
|
||||||
|
"pram": {},
|
||||||
|
"vcpu": {},
|
||||||
|
"vram": {},
|
||||||
|
"vm": {},
|
||||||
|
"flavors": {},
|
||||||
|
}
|
||||||
cache.clear()
|
cache.clear()
|
||||||
cache.set("dashboard_stats", cached, timeout=120)
|
cache.set("dashboard_stats", cached, timeout=120)
|
||||||
request = self.factory.get("/api/stats/")
|
request = self.factory.get("/api/stats/")
|
||||||
@@ -219,13 +256,24 @@ class ApiAuditsTest(TestCase):
|
|||||||
"scope": "Full Cluster",
|
"scope": "Full Cluster",
|
||||||
"cpu_weight": "1.0",
|
"cpu_weight": "1.0",
|
||||||
"ram_weight": "1.0",
|
"ram_weight": "1.0",
|
||||||
"migrations": [{"instanceName": "i1", "source": "h0", "destination": "h1", "flavor": "m1.small", "impact": "Low"}],
|
"migrations": [
|
||||||
|
{
|
||||||
|
"instanceName": "i1",
|
||||||
|
"source": "h0",
|
||||||
|
"destination": "h1",
|
||||||
|
"flavor": "m1.small",
|
||||||
|
"impact": "Low",
|
||||||
|
}
|
||||||
|
],
|
||||||
"host_labels": ["h0", "h1"],
|
"host_labels": ["h0", "h1"],
|
||||||
"cpu_current": [30.0, 40.0],
|
"cpu_current": [30.0, 40.0],
|
||||||
"cpu_projected": [35.0, 35.0],
|
"cpu_projected": [35.0, 35.0],
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
mock_get_current_cluster_cpu.return_value = {"host_labels": ["h0", "h1"], "cpu_current": [30.0, 40.0]}
|
mock_get_current_cluster_cpu.return_value = {
|
||||||
|
"host_labels": ["h0", "h1"],
|
||||||
|
"cpu_current": [30.0, 40.0],
|
||||||
|
}
|
||||||
cache.clear()
|
cache.clear()
|
||||||
request = self.factory.get("/api/audits/")
|
request = self.factory.get("/api/audits/")
|
||||||
with patch("dashboard.views.settings") as mock_settings:
|
with patch("dashboard.views.settings") as mock_settings:
|
||||||
@@ -246,9 +294,20 @@ class ApiAuditsTest(TestCase):
|
|||||||
@patch("dashboard.views.get_current_cluster_cpu")
|
@patch("dashboard.views.get_current_cluster_cpu")
|
||||||
@patch("dashboard.views.collect_audits")
|
@patch("dashboard.views.collect_audits")
|
||||||
@patch("dashboard.views.settings")
|
@patch("dashboard.views.settings")
|
||||||
def test_api_audits_uses_cache(self, mock_settings, mock_collect_audits, mock_get_current_cluster_cpu):
|
def test_api_audits_uses_cache(
|
||||||
|
self, mock_settings, mock_collect_audits, mock_get_current_cluster_cpu
|
||||||
|
):
|
||||||
mock_settings.DASHBOARD_CACHE_TTL = 120
|
mock_settings.DASHBOARD_CACHE_TTL = 120
|
||||||
cached_audits = [{"id": "cached-1", "name": "Cached Audit", "migrations": "[]", "host_labels": "[]", "cpu_current": "[]", "cpu_projected": "[]"}]
|
cached_audits = [
|
||||||
|
{
|
||||||
|
"id": "cached-1",
|
||||||
|
"name": "Cached Audit",
|
||||||
|
"migrations": "[]",
|
||||||
|
"host_labels": "[]",
|
||||||
|
"cpu_current": "[]",
|
||||||
|
"cpu_projected": "[]",
|
||||||
|
}
|
||||||
|
]
|
||||||
cached_cluster = {"host_labels": ["cached-h0"], "cpu_current": [10.0]}
|
cached_cluster = {"host_labels": ["cached-h0"], "cpu_current": [10.0]}
|
||||||
cache.clear()
|
cache.clear()
|
||||||
cache.set("dashboard_audits", cached_audits, timeout=120)
|
cache.set("dashboard_audits", cached_audits, timeout=120)
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
from django.urls import path
|
from django.urls import path
|
||||||
|
|
||||||
from . import views
|
from . import views
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path('', views.index, name='index'),
|
path("", views.index, name="index"),
|
||||||
path('api/stats/', views.api_stats),
|
path("api/stats/", views.api_stats),
|
||||||
path('api/audits/', views.api_audits),
|
path("api/audits/", views.api_audits),
|
||||||
path('api/source-status/', views.api_source_status),
|
path("api/source-status/", views.api_source_status),
|
||||||
]
|
]
|
||||||
@@ -5,23 +5,32 @@ from django.conf import settings
|
|||||||
from django.core.cache import cache
|
from django.core.cache import cache
|
||||||
from django.http import JsonResponse
|
from django.http import JsonResponse
|
||||||
from django.shortcuts import render
|
from django.shortcuts import render
|
||||||
|
|
||||||
|
from dashboard.mock_data import get_mock_context
|
||||||
|
from dashboard.openstack_utils.audits import get_audits, get_current_cluster_cpu
|
||||||
from dashboard.openstack_utils.connect import check_openstack, get_connection
|
from dashboard.openstack_utils.connect import check_openstack, get_connection
|
||||||
from dashboard.openstack_utils.flavor import get_flavor_list
|
from dashboard.openstack_utils.flavor import get_flavor_list
|
||||||
from dashboard.prometheus_utils.query import check_prometheus, 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
|
|
||||||
|
|
||||||
# Prometheus queries run in parallel (query_key -> query string)
|
# Prometheus queries run in parallel (query_key -> query string)
|
||||||
_PROMETHEUS_QUERIES = {
|
_PROMETHEUS_QUERIES = {
|
||||||
"hosts_total": "count(node_exporter_build_info{job='node_exporter_compute'})",
|
"hosts_total": "count(node_exporter_build_info{job='node_exporter_compute'})",
|
||||||
"pcpu_total": "sum(count(node_cpu_seconds_total{job='node_exporter_compute', mode='idle'}) without (cpu,mode))",
|
"pcpu_total": (
|
||||||
|
"sum(count(node_cpu_seconds_total{job='node_exporter_compute', mode='idle'}) "
|
||||||
|
"without (cpu,mode))"
|
||||||
|
),
|
||||||
"pcpu_usage": "sum(node_load5{job='node_exporter_compute'})",
|
"pcpu_usage": "sum(node_load5{job='node_exporter_compute'})",
|
||||||
"vcpu_allocated": "sum(libvirt_domain_info_virtual_cpus)",
|
"vcpu_allocated": "sum(libvirt_domain_info_virtual_cpus)",
|
||||||
"vcpu_overcommit_max": "avg(openstack_placement_resource_allocation_ratio{resourcetype='VCPU'})",
|
"vcpu_overcommit_max": (
|
||||||
|
"avg(openstack_placement_resource_allocation_ratio{resourcetype='VCPU'})"
|
||||||
|
),
|
||||||
"pram_total": "sum(node_memory_MemTotal_bytes{job='node_exporter_compute'})",
|
"pram_total": "sum(node_memory_MemTotal_bytes{job='node_exporter_compute'})",
|
||||||
"pram_usage": "sum(node_memory_Active_bytes{job='node_exporter_compute'})",
|
"pram_usage": "sum(node_memory_Active_bytes{job='node_exporter_compute'})",
|
||||||
"vram_allocated": "sum(libvirt_domain_info_maximum_memory_bytes)",
|
"vram_allocated": "sum(libvirt_domain_info_maximum_memory_bytes)",
|
||||||
"vram_overcommit_max": "avg(avg_over_time(openstack_placement_resource_allocation_ratio{resourcetype='MEMORY_MB'}[5m]))",
|
"vram_overcommit_max": (
|
||||||
|
"avg(avg_over_time("
|
||||||
|
"openstack_placement_resource_allocation_ratio{resourcetype='MEMORY_MB'}[5m]))"
|
||||||
|
),
|
||||||
"vm_count": "sum(libvirt_domain_state_code)",
|
"vm_count": "sum(libvirt_domain_state_code)",
|
||||||
"vm_active": "sum(libvirt_domain_state_code{stateDesc='the domain is running'})",
|
"vm_active": "sum(libvirt_domain_state_code{stateDesc='the domain is running'})",
|
||||||
}
|
}
|
||||||
@@ -44,7 +53,9 @@ def _fetch_prometheus_metrics():
|
|||||||
else:
|
else:
|
||||||
result[key] = int(raw)
|
result[key] = int(raw)
|
||||||
except (ValueError, TypeError):
|
except (ValueError, TypeError):
|
||||||
result[key] = 0 if key in ("pcpu_usage", "vcpu_overcommit_max", "vram_overcommit_max") else 0
|
result[key] = (
|
||||||
|
0 if key in ("pcpu_usage", "vcpu_overcommit_max", "vram_overcommit_max") else 0
|
||||||
|
)
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
@@ -221,8 +232,22 @@ def _skeleton_context():
|
|||||||
"region": {"name": "—", "hosts_total": 0},
|
"region": {"name": "—", "hosts_total": 0},
|
||||||
"pcpu": {"total": 0, "usage": 0, "free": 0, "used_percentage": 0},
|
"pcpu": {"total": 0, "usage": 0, "free": 0, "used_percentage": 0},
|
||||||
"pram": {"total": 0, "usage": 0, "free": 0, "used_percentage": 0},
|
"pram": {"total": 0, "usage": 0, "free": 0, "used_percentage": 0},
|
||||||
"vcpu": {"total": 0, "allocated": 0, "free": 0, "allocated_percentage": 0, "overcommit_ratio": 0, "overcommit_max": 0},
|
"vcpu": {
|
||||||
"vram": {"total": 0, "allocated": 0, "free": 0, "allocated_percentage": 0, "overcommit_ratio": 0, "overcommit_max": 0},
|
"total": 0,
|
||||||
|
"allocated": 0,
|
||||||
|
"free": 0,
|
||||||
|
"allocated_percentage": 0,
|
||||||
|
"overcommit_ratio": 0,
|
||||||
|
"overcommit_max": 0,
|
||||||
|
},
|
||||||
|
"vram": {
|
||||||
|
"total": 0,
|
||||||
|
"allocated": 0,
|
||||||
|
"free": 0,
|
||||||
|
"allocated_percentage": 0,
|
||||||
|
"overcommit_ratio": 0,
|
||||||
|
"overcommit_max": 0,
|
||||||
|
},
|
||||||
"vm": {"count": 0, "active": 0, "stopped": 0, "avg_cpu": 0, "avg_ram": 0, "density": 0},
|
"vm": {"count": 0, "active": 0, "stopped": 0, "avg_cpu": 0, "avg_ram": 0, "density": 0},
|
||||||
"flavors": empty_flavors,
|
"flavors": empty_flavors,
|
||||||
"audits": [],
|
"audits": [],
|
||||||
@@ -270,10 +295,12 @@ def api_audits(request):
|
|||||||
def api_source_status(request):
|
def api_source_status(request):
|
||||||
"""Return status of Prometheus and OpenStack data sources (ok / error / mock)."""
|
"""Return status of Prometheus and OpenStack data sources (ok / error / mock)."""
|
||||||
if getattr(settings, "USE_MOCK_DATA", False):
|
if getattr(settings, "USE_MOCK_DATA", False):
|
||||||
return JsonResponse({
|
return JsonResponse(
|
||||||
"prometheus": {"status": "mock"},
|
{
|
||||||
"openstack": {"status": "mock"},
|
"prometheus": {"status": "mock"},
|
||||||
})
|
"openstack": {"status": "mock"},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
cache_key = "dashboard_source_status"
|
cache_key = "dashboard_source_status"
|
||||||
cache_ttl = getattr(settings, "SOURCE_STATUS_CACHE_TTL", 30)
|
cache_ttl = getattr(settings, "SOURCE_STATUS_CACHE_TTL", 30)
|
||||||
|
|||||||
@@ -1,12 +1,13 @@
|
|||||||
#!/usr/bin/env python
|
#!/usr/bin/env python
|
||||||
"""Django's command-line utility for administrative tasks."""
|
"""Django's command-line utility for administrative tasks."""
|
||||||
|
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
"""Run administrative tasks."""
|
"""Run administrative tasks."""
|
||||||
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'watcher_visio.settings')
|
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "watcher_visio.settings")
|
||||||
try:
|
try:
|
||||||
from django.core.management import execute_from_command_line
|
from django.core.management import execute_from_command_line
|
||||||
except ImportError as exc:
|
except ImportError as exc:
|
||||||
@@ -18,5 +19,5 @@ def main():
|
|||||||
execute_from_command_line(sys.argv)
|
execute_from_command_line(sys.argv)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == "__main__":
|
||||||
main()
|
main()
|
||||||
|
|||||||
@@ -11,6 +11,6 @@ import os
|
|||||||
|
|
||||||
from django.core.asgi import get_asgi_application
|
from django.core.asgi import get_asgi_application
|
||||||
|
|
||||||
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'watcher_visio.settings')
|
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "watcher_visio.settings")
|
||||||
|
|
||||||
application = get_asgi_application()
|
application = get_asgi_application()
|
||||||
|
|||||||
@@ -24,31 +24,31 @@ USE_MOCK_DATA = os.environ.get("USE_MOCK_DATA", "false").lower() in ("1", "true"
|
|||||||
# See https://docs.djangoproject.com/en/5.2/howto/deployment/checklist/
|
# See https://docs.djangoproject.com/en/5.2/howto/deployment/checklist/
|
||||||
|
|
||||||
# SECURITY WARNING: keep the secret key used in production secret!
|
# SECURITY WARNING: keep the secret key used in production secret!
|
||||||
SECRET_KEY = 'django-insecure-747*14ir*49hoo6c2225)kxr%4^am0ub_s-m^_7i4cctu)v$g8'
|
SECRET_KEY = "django-insecure-747*14ir*49hoo6c2225)kxr%4^am0ub_s-m^_7i4cctu)v$g8"
|
||||||
|
|
||||||
# SECURITY WARNING: don't run with debug turned on in production!
|
# SECURITY WARNING: don't run with debug turned on in production!
|
||||||
DEBUG = True
|
DEBUG = True
|
||||||
|
|
||||||
ALLOWED_HOSTS = ['*']
|
ALLOWED_HOSTS = ["*"]
|
||||||
|
|
||||||
|
|
||||||
# Application definition
|
# Application definition
|
||||||
|
|
||||||
INSTALLED_APPS = [
|
INSTALLED_APPS = [
|
||||||
'django.contrib.admin',
|
"django.contrib.admin",
|
||||||
'django.contrib.auth',
|
"django.contrib.auth",
|
||||||
'django.contrib.contenttypes',
|
"django.contrib.contenttypes",
|
||||||
'django.contrib.sessions',
|
"django.contrib.sessions",
|
||||||
'django.contrib.messages',
|
"django.contrib.messages",
|
||||||
'django.contrib.staticfiles',
|
"django.contrib.staticfiles",
|
||||||
'dashboard',
|
"dashboard",
|
||||||
]
|
]
|
||||||
|
|
||||||
# Prometheus settings (environment override recommended)
|
# Prometheus settings (environment override recommended)
|
||||||
PROMETHEUS_URL = "http://10.226.74.53:9090/"
|
PROMETHEUS_URL = "http://10.226.74.53:9090/"
|
||||||
PROMETHEUS_METRICS = {
|
PROMETHEUS_METRICS = {
|
||||||
"cpu_usage": "rate(libvirt_domain_info_cpu_time_seconds_total)[300s]",
|
"cpu_usage": "rate(libvirt_domain_info_cpu_time_seconds_total)[300s]",
|
||||||
"ram_usage": "avg_over_time(libvirt_domain_info_memory_usage_bytes[300s]"
|
"ram_usage": "avg_over_time(libvirt_domain_info_memory_usage_bytes[300s]",
|
||||||
}
|
}
|
||||||
|
|
||||||
# Openstack cloud settings
|
# Openstack cloud settings
|
||||||
@@ -60,45 +60,45 @@ WATCHER_ENDPOINT_NAME = "infra-optim"
|
|||||||
WATCHER_INTERFACE_NAME = "public"
|
WATCHER_INTERFACE_NAME = "public"
|
||||||
|
|
||||||
MIDDLEWARE = [
|
MIDDLEWARE = [
|
||||||
'django.middleware.security.SecurityMiddleware',
|
"django.middleware.security.SecurityMiddleware",
|
||||||
'django.contrib.sessions.middleware.SessionMiddleware',
|
"django.contrib.sessions.middleware.SessionMiddleware",
|
||||||
'django.middleware.common.CommonMiddleware',
|
"django.middleware.common.CommonMiddleware",
|
||||||
'django.middleware.csrf.CsrfViewMiddleware',
|
"django.middleware.csrf.CsrfViewMiddleware",
|
||||||
'django.contrib.auth.middleware.AuthenticationMiddleware',
|
"django.contrib.auth.middleware.AuthenticationMiddleware",
|
||||||
'django.contrib.messages.middleware.MessageMiddleware',
|
"django.contrib.messages.middleware.MessageMiddleware",
|
||||||
'django.middleware.clickjacking.XFrameOptionsMiddleware',
|
"django.middleware.clickjacking.XFrameOptionsMiddleware",
|
||||||
]
|
]
|
||||||
|
|
||||||
# COOP ignored on non-HTTPS / non-localhost; disable to avoid console warning
|
# COOP ignored on non-HTTPS / non-localhost; disable to avoid console warning
|
||||||
SECURE_CROSS_ORIGIN_OPENER_POLICY = None
|
SECURE_CROSS_ORIGIN_OPENER_POLICY = None
|
||||||
|
|
||||||
ROOT_URLCONF = 'watcher_visio.urls'
|
ROOT_URLCONF = "watcher_visio.urls"
|
||||||
|
|
||||||
TEMPLATES = [
|
TEMPLATES = [
|
||||||
{
|
{
|
||||||
'BACKEND': 'django.template.backends.django.DjangoTemplates',
|
"BACKEND": "django.template.backends.django.DjangoTemplates",
|
||||||
'DIRS': [BASE_DIR / 'templates'],
|
"DIRS": [BASE_DIR / "templates"],
|
||||||
'APP_DIRS': True,
|
"APP_DIRS": True,
|
||||||
'OPTIONS': {
|
"OPTIONS": {
|
||||||
'context_processors': [
|
"context_processors": [
|
||||||
'django.template.context_processors.request',
|
"django.template.context_processors.request",
|
||||||
'django.contrib.auth.context_processors.auth',
|
"django.contrib.auth.context_processors.auth",
|
||||||
'django.contrib.messages.context_processors.messages',
|
"django.contrib.messages.context_processors.messages",
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
WSGI_APPLICATION = 'watcher_visio.wsgi.application'
|
WSGI_APPLICATION = "watcher_visio.wsgi.application"
|
||||||
|
|
||||||
|
|
||||||
# Database
|
# Database
|
||||||
# https://docs.djangoproject.com/en/5.2/ref/settings/#databases
|
# https://docs.djangoproject.com/en/5.2/ref/settings/#databases
|
||||||
|
|
||||||
DATABASES = {
|
DATABASES = {
|
||||||
'default': {
|
"default": {
|
||||||
'ENGINE': 'django.db.backends.sqlite3',
|
"ENGINE": "django.db.backends.sqlite3",
|
||||||
'NAME': BASE_DIR / 'db.sqlite3',
|
"NAME": BASE_DIR / "db.sqlite3",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -108,16 +108,16 @@ DATABASES = {
|
|||||||
|
|
||||||
AUTH_PASSWORD_VALIDATORS = [
|
AUTH_PASSWORD_VALIDATORS = [
|
||||||
{
|
{
|
||||||
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
|
"NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
|
"NAME": "django.contrib.auth.password_validation.MinimumLengthValidator",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
|
"NAME": "django.contrib.auth.password_validation.CommonPasswordValidator",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
|
"NAME": "django.contrib.auth.password_validation.NumericPasswordValidator",
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -125,9 +125,9 @@ AUTH_PASSWORD_VALIDATORS = [
|
|||||||
# Internationalization
|
# Internationalization
|
||||||
# https://docs.djangoproject.com/en/5.2/topics/i18n/
|
# https://docs.djangoproject.com/en/5.2/topics/i18n/
|
||||||
|
|
||||||
LANGUAGE_CODE = 'en-us'
|
LANGUAGE_CODE = "en-us"
|
||||||
|
|
||||||
TIME_ZONE = 'UTC'
|
TIME_ZONE = "UTC"
|
||||||
|
|
||||||
USE_I18N = True
|
USE_I18N = True
|
||||||
|
|
||||||
@@ -137,7 +137,7 @@ USE_TZ = True
|
|||||||
# Static files (CSS, JavaScript, Images)
|
# Static files (CSS, JavaScript, Images)
|
||||||
# https://docs.djangoproject.com/en/5.2/howto/static-files/
|
# https://docs.djangoproject.com/en/5.2/howto/static-files/
|
||||||
|
|
||||||
STATIC_URL = '/static/'
|
STATIC_URL = "/static/"
|
||||||
|
|
||||||
STATICFILES_DIRS = [
|
STATICFILES_DIRS = [
|
||||||
BASE_DIR / "static",
|
BASE_DIR / "static",
|
||||||
@@ -148,13 +148,13 @@ STATIC_ROOT = BASE_DIR / "staticfiles"
|
|||||||
# Default primary key field type
|
# Default primary key field type
|
||||||
# https://docs.djangoproject.com/en/5.2/ref/settings/#default-auto-field
|
# https://docs.djangoproject.com/en/5.2/ref/settings/#default-auto-field
|
||||||
|
|
||||||
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
|
DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"
|
||||||
|
|
||||||
# Dashboard cache (reduces load on OpenStack/Prometheus and allows concurrent users)
|
# Dashboard cache (reduces load on OpenStack/Prometheus and allows concurrent users)
|
||||||
CACHES = {
|
CACHES = {
|
||||||
'default': {
|
"default": {
|
||||||
'BACKEND': 'django.core.cache.backends.locmem.LocMemCache',
|
"BACKEND": "django.core.cache.backends.locmem.LocMemCache",
|
||||||
'LOCATION': 'watcher-visio-dashboard',
|
"LOCATION": "watcher-visio-dashboard",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
DASHBOARD_CACHE_TTL = 120 # seconds
|
DASHBOARD_CACHE_TTL = 120 # seconds
|
||||||
|
|||||||
@@ -14,13 +14,17 @@ Including another URLconf
|
|||||||
1. Import the include() function: from django.urls import include, path
|
1. Import the include() function: from django.urls import include, path
|
||||||
2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
|
2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
from django.urls import path, include
|
from django.urls import include, path
|
||||||
from django.views.generic import RedirectView
|
from django.views.generic import RedirectView
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path('admin/', admin.site.urls),
|
path("admin/", admin.site.urls),
|
||||||
path('favicon.ico', RedirectView.as_view(url=settings.STATIC_URL + 'favicon.ico', permanent=False)),
|
path(
|
||||||
path('', include('dashboard.urls')),
|
"favicon.ico",
|
||||||
|
RedirectView.as_view(url=settings.STATIC_URL + "favicon.ico", permanent=False),
|
||||||
|
),
|
||||||
|
path("", include("dashboard.urls")),
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -11,6 +11,6 @@ import os
|
|||||||
|
|
||||||
from django.core.wsgi import get_wsgi_application
|
from django.core.wsgi import get_wsgi_application
|
||||||
|
|
||||||
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'watcher_visio.settings')
|
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "watcher_visio.settings")
|
||||||
|
|
||||||
application = get_wsgi_application()
|
application = get_wsgi_application()
|
||||||
|
|||||||
Reference in New Issue
Block a user