Skip to content

Commit

Permalink
Configure Grafana DashBoards dinamically (#392)
Browse files Browse the repository at this point in the history
If an exporter is not present, there is no need of rendering a
dashboard for an specific exporter. E.g: a machine without NVIDIA
doesn't need to render an empty Dashboard for GPU

Fix: #366
  • Loading branch information
gabrielcocenza authored Feb 14, 2025
1 parent 52a34ea commit 4e64e28
Show file tree
Hide file tree
Showing 6 changed files with 64 additions and 3 deletions.
13 changes: 13 additions & 0 deletions src/charm.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ def __init__(self, *args: Any) -> None:
self,
refresh_events=[self.on.config_changed, self.on.upgrade_charm],
scrape_configs=self._scrape_config,
dashboard_dirs=self._dashboards(),
)

self.num_cos_agent_relations = self.get_num_cos_agent_relations("cos-agent")
Expand Down Expand Up @@ -326,6 +327,18 @@ def _scrape_config(self) -> List[Dict[str, Any]]:
)
return scrape_config

def _dashboards(self) -> List[str]:
"""Create the dashboards as needed."""
dashboards = []
for exporter in self.exporters:
if isinstance(exporter, HardwareExporter):
dashboards.append("./src/dashboards_hardware_exporter")
if isinstance(exporter, SmartCtlExporter):
dashboards.append("./src/dashboards_smart_ctl")
if isinstance(exporter, DCGMExporter):
dashboards.append("./src/dashboards_dcgm")
return dashboards

@property
def cos_agent_related(self) -> bool:
"""Return True if cos-agent relation is present."""
Expand Down
File renamed without changes.
File renamed without changes.
File renamed without changes.
49 changes: 46 additions & 3 deletions tests/unit/test_charm.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,8 @@ def _get_notice_count(self, hook):
notice_count += 1
return notice_count

def test_harness(self) -> None:
@mock.patch("charm.HardwareObserverCharm._dashboards")
def test_harness(self, _) -> None:
"""Test charm initialize."""
self.harness.begin()
self.assertFalse(self.harness.charm._stored.resource_installed)
Expand Down Expand Up @@ -77,11 +78,19 @@ def test_harness(self) -> None:
),
]
)
@mock.patch("charm.HardwareObserverCharm._dashboards")
@mock.patch("charm.DCGMExporter")
@mock.patch("charm.SmartCtlExporter")
@mock.patch("charm.HardwareExporter")
def test_exporters(
self, _, stored_tools, expect, mock_hw_exporter, mock_smart_exporter, mock_dcgm_exporter
self,
_,
stored_tools,
expect,
mock_hw_exporter,
mock_smart_exporter,
mock_dcgm_exporter,
mock_dashboards,
):
with mock.patch(
"charm.HardwareObserverCharm.stored_tools",
Expand Down Expand Up @@ -452,6 +461,7 @@ def test_update_status( # noqa: C901
),
]
)
@mock.patch("charm.HardwareObserverCharm._dashboards")
@mock.patch(
"charm.detect_available_tools",
)
Expand All @@ -462,6 +472,7 @@ def test_detect_hardware_action(
detected_available_tools,
expect_output,
mock_detect_available_tools,
_,
) -> None:
"""Test action detect-hardware."""
mock_detect_available_tools.return_value = detected_available_tools
Expand Down Expand Up @@ -793,7 +804,8 @@ def test_validate_configs(
result = self.harness.charm.validate_configs()
self.assertEqual(result, expect)

def test_stored_tools_remove_legacy_smartctl(self):
@mock.patch("charm.HardwareObserverCharm._dashboards")
def test_stored_tools_remove_legacy_smartctl(self, _):
self.harness.begin()
self.harness.charm._stored.stored_tools = {"smartctl"}
assert self.harness.charm.stored_tools == set()
Expand Down Expand Up @@ -832,3 +844,34 @@ def test_scrape_config_no_specific_hardware(
{"scrape_timeout": "10s"},
{"metrics_path": "/metrics", "static_configs": [{"targets": ["localhost:10201"]}]},
]

@mock.patch("service.get_bmc_address")
@mock.patch("charm.HardwareObserverCharm.exporters", new_callable=mock.PropertyMock)
def test_dashboards(self, mock_exporters, _):
self.harness.begin()
config = self.harness.charm.model.config
hw_exporter = HardwareExporter(Path(), config, set())
smartctl_exporter = SmartCtlExporter(config)
dcgm_exporter = DCGMExporter(config)

mock_exporters.return_value = [hw_exporter, smartctl_exporter, dcgm_exporter]

assert self.harness.charm._dashboards() == [
"./src/dashboards_hardware_exporter",
"./src/dashboards_smart_ctl",
"./src/dashboards_dcgm",
]

@mock.patch("charm.HardwareObserverCharm.exporters", new_callable=mock.PropertyMock)
def test_dashboards_no_specific_hardware(
self,
mock_exporters,
):
# simulate a hardware that does not have NVIDIA or tools to install hw exporter
self.harness.begin()
config = self.harness.charm.model.config
smartctl_exporter = SmartCtlExporter(config)

mock_exporters.return_value = [smartctl_exporter]

assert self.harness.charm._dashboards() == ["./src/dashboards_smart_ctl"]
5 changes: 5 additions & 0 deletions tests/unit/test_hw_tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -125,9 +125,14 @@ def test_make_executable_error_handling(self, mock_os):

class TestHWToolHelper(unittest.TestCase):
def setUp(self):
mock_dashboards = mock.patch.object(HardwareObserverCharm, "_dashboards")
self.mock_dashboards = mock_dashboards.start()

self.harness = ops.testing.Harness(HardwareObserverCharm)
self.harness.begin()

self.addCleanup(self.harness.cleanup)
self.addCleanup(mock_dashboards.stop)

self.hw_tool_helper = HWToolHelper()

Expand Down

0 comments on commit 4e64e28

Please sign in to comment.