|
| 1 | +#!/usr/bin/env python3 |
| 2 | +# Copyright (C) 2026 Checkmk GmbH - License: GNU General Public License v2 |
| 3 | +# This file is part of Checkmk (https://checkmk.com). It is subject to the terms and |
| 4 | +# conditions defined in the file COPYING, which is part of this source code package. |
| 5 | + |
| 6 | +# The fetcher stand-ins are constructed with the endpoint's keyword arguments, which only |
| 7 | +# Callable[..., ...] can spell; the sibling endpoint test does the same. |
| 8 | +# mypy: disable-error-code="explicit-any" |
| 9 | + |
| 10 | +from collections.abc import Callable, Mapping |
| 11 | + |
| 12 | +import pytest |
| 13 | + |
| 14 | +from cmk.graphing_engine import HostName, MetricName, Service, ServiceName |
| 15 | +from cmk.gui.graphing.openapi import translate_metric_names as endpoint_module |
| 16 | +from cmk.livestatus_client import MKLivestatusSocketError |
| 17 | +from tests.testlib.rest_api_client import ClientRegistry |
| 18 | + |
| 19 | +# The CPU utilization check reports the raw perf-data name "wait", which the collection |
| 20 | +# plug-in renames to the metric "io_wait". |
| 21 | +_RAW_NAME = MetricName("wait") |
| 22 | +_CANONICAL_NAME = MetricName("io_wait") |
| 23 | + |
| 24 | + |
| 25 | +def _fetcher_returning( |
| 26 | + mapping: Mapping[Service, Mapping[MetricName, MetricName]], |
| 27 | + asked_about: dict[str, object], |
| 28 | +) -> Callable[..., Callable[[], Mapping[Service, Mapping[MetricName, MetricName]]]]: |
| 29 | + """Stand in for RRDFetchMetricNameMapping, recording the service it was asked about.""" |
| 30 | + |
| 31 | + def _make(**kwargs: object) -> Callable[[], Mapping[Service, Mapping[MetricName, MetricName]]]: |
| 32 | + asked_about.update(kwargs) |
| 33 | + return lambda: mapping |
| 34 | + |
| 35 | + return _make |
| 36 | + |
| 37 | + |
| 38 | +def test_translate_metric_names_returns_the_fetched_mapping( |
| 39 | + clients: ClientRegistry, monkeypatch: pytest.MonkeyPatch |
| 40 | +) -> None: |
| 41 | + asked_about: dict[str, object] = {} |
| 42 | + monkeypatch.setattr( |
| 43 | + endpoint_module, |
| 44 | + "RRDFetchMetricNameMapping", |
| 45 | + _fetcher_returning( |
| 46 | + { |
| 47 | + Service( |
| 48 | + host_name=HostName("my-host"), |
| 49 | + service_name=ServiceName("CPU utilization"), |
| 50 | + ): {_RAW_NAME: _CANONICAL_NAME} |
| 51 | + }, |
| 52 | + asked_about, |
| 53 | + ), |
| 54 | + ) |
| 55 | + |
| 56 | + resp = clients.Graph.translate_metric_names( |
| 57 | + hostname="my-host", service_description="CPU utilization" |
| 58 | + ) |
| 59 | + |
| 60 | + assert resp.json["metric_names"] == {"wait": "io_wait"} |
| 61 | + # The mapping has to be the one for the posted service, not for whatever the fetcher was |
| 62 | + # handed: without this the endpoint could ignore the request body and still pass. |
| 63 | + assert asked_about["host_name"] == "my-host" |
| 64 | + assert asked_about["service_name"] == "CPU utilization" |
| 65 | + |
| 66 | + |
| 67 | +def test_translate_metric_names_of_an_unknown_service_is_an_empty_mapping( |
| 68 | + clients: ClientRegistry, monkeypatch: pytest.MonkeyPatch |
| 69 | +) -> None: |
| 70 | + # An unmonitored host or service resolves to no rows. That is an empty answer, not an error: |
| 71 | + # the caller is the one that turns it into a message. |
| 72 | + monkeypatch.setattr(endpoint_module, "RRDFetchMetricNameMapping", _fetcher_returning({}, {})) |
| 73 | + |
| 74 | + resp = clients.Graph.translate_metric_names( |
| 75 | + hostname="my-host", service_description="No such service" |
| 76 | + ) |
| 77 | + |
| 78 | + assert resp.json["metric_names"] == {} |
| 79 | + |
| 80 | + |
| 81 | +def test_translate_metric_names_scopes_the_fetch_to_a_site_the_user_may_see( |
| 82 | + clients: ClientRegistry, monkeypatch: pytest.MonkeyPatch |
| 83 | +) -> None: |
| 84 | + asked_about: dict[str, object] = {} |
| 85 | + monkeypatch.setattr( |
| 86 | + endpoint_module, "RRDFetchMetricNameMapping", _fetcher_returning({}, asked_about) |
| 87 | + ) |
| 88 | + |
| 89 | + clients.Graph.translate_metric_names( |
| 90 | + hostname="my-host", service_description="CPU utilization", site="NO_SITE" |
| 91 | + ) |
| 92 | + |
| 93 | + assert asked_about["site_id"] == "NO_SITE" |
| 94 | + |
| 95 | + |
| 96 | +def test_translate_metric_names_of_a_site_the_user_may_not_see_is_rejected( |
| 97 | + clients: ClientRegistry, monkeypatch: pytest.MonkeyPatch |
| 98 | +) -> None: |
| 99 | + # A site outside the user's list would otherwise scope the query to nothing and answer 200 with |
| 100 | + # an empty mapping, which reads exactly like a service that has no perf data. |
| 101 | + monkeypatch.setattr(endpoint_module, "RRDFetchMetricNameMapping", _fetcher_returning({}, {})) |
| 102 | + |
| 103 | + resp = clients.Graph.translate_metric_names( |
| 104 | + hostname="my-host", |
| 105 | + service_description="CPU utilization", |
| 106 | + site="no-such-site", |
| 107 | + expect_ok=False, |
| 108 | + ) |
| 109 | + |
| 110 | + assert resp.status_code == 400 |
| 111 | + |
| 112 | + |
| 113 | +def test_translate_metric_names_livestatus_failure_is_503( |
| 114 | + clients: ClientRegistry, monkeypatch: pytest.MonkeyPatch |
| 115 | +) -> None: |
| 116 | + def _raise( |
| 117 | + **_kwargs: object, |
| 118 | + ) -> Callable[[], Mapping[Service, Mapping[MetricName, MetricName]]]: |
| 119 | + def _fetch() -> Mapping[Service, Mapping[MetricName, MetricName]]: |
| 120 | + raise MKLivestatusSocketError("connection refused") |
| 121 | + |
| 122 | + return _fetch |
| 123 | + |
| 124 | + monkeypatch.setattr(endpoint_module, "RRDFetchMetricNameMapping", _raise) |
| 125 | + |
| 126 | + resp = clients.Graph.translate_metric_names( |
| 127 | + hostname="my-host", service_description="CPU utilization", expect_ok=False |
| 128 | + ) |
| 129 | + |
| 130 | + assert resp.status_code == 503 |
| 131 | + assert "connection refused" in resp.json["detail"] |
0 commit comments