Files
watcher/watcher/tests/decision_engine/datasources/test_prometheus_helper.py
Jaromir Wysoglad 8309d9848a Add Aetos datasource
Implement the spec for multi-tenancy support for metrics. This adds
a new 'Aetos' datasource very similar to the current Prometheus
datasource. Because of that, the original PrometheusHelper class
was split into two classes and the base class is used for
PrometheusHelper and for AetosHelper. Except for the split, there
is one more change to the original PrometheusHelper class code, which
is the addition and use of the _get_fqdn_label() and
_get_instance_uuid_label() methods.

As part of the change, I refactored the current prometheus datasource
unit tests. Most of them are now used to test the PrometheusBase class
with minimal changes. Changes I've made to the original tests:

- the ones that can be be used to test the base class are moved into the
  TestPrometheusBase class
- the _setup_prometheus_client, _get_instance_uuid_label and
  _get_fqdn_label functions are mocked in the base class tests.
  Their concrete implementations are tested in each datasource tests
  separately.
- a self._create_helper() is used to instantiate the helper class with
  correct mocking.
- all config value modification is the original tests got moved out and
  instead of modifying the config values, the _get_* methods are mocked
  to return the wanted values
- to keep similar test coverage, config retrieval is tested for each
  concrete class by testing the _get_* methods.

New watcher-aetos-integration and watcher-aetos-integration-realdata
zuul jobs are added to test the new datasource. These use the same set
of tempest tests as the current watcher-prometheus-integration jobs.
The only difference is the environment setup and the Watcher config,
so that the job deploys Aetos and Watcher uses it instead of accessing
Prometheus directly.

At first this was generated by asking cursor to implement the linked spec
with some additional prompts for some smaller changes. Afterwards I manually
went through the code doing some cleanups, ensuring it complies with
PEP8 and hacking and so on. Later on I manually adjusted the code to use
the latest observabilityclient changes.
The zuul job was also mostly generated by cursor.

Implements: https://blueprints.launchpad.net/watcher/+spec/prometheus-multitenancy-support

Generated-By: Cursor with claude-4-sonnet model
Change-Id: I72c2171f72819bbde6c9cbbf565ee895e5d2bd53
Signed-off-by: Jaromir Wysoglad <jwysogla@redhat.com>
2025-08-14 02:27:24 -04:00

169 lines
6.6 KiB
Python

# Copyright 2024 Red Hat, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
#
from unittest import mock
from observabilityclient import prometheus_client
from oslo_config import cfg
from watcher.common import exception
from watcher.decision_engine.datasources import prometheus as prometheus_helper
from watcher.tests import base
class TestPrometheusHelper(base.BaseTestCase):
def setUp(self):
super(TestPrometheusHelper, self).setUp()
cfg.CONF.prometheus_client.host = "foobarbaz"
cfg.CONF.prometheus_client.port = "1234"
with mock.patch.object(
prometheus_helper.PrometheusHelper,
'_setup_prometheus_client'
):
self.helper = prometheus_helper.PrometheusHelper()
# Set up patches for all methods used inside the
# _setup_prometheus_client
self.mock_init = mock.patch.object(
prometheus_client.PrometheusAPIClient, '__init__',
return_value=None).start()
self.addCleanup(self.mock_init.stop)
self.mock_set_ca_cert = mock.patch.object(
prometheus_client.PrometheusAPIClient, 'set_ca_cert').start()
self.addCleanup(self.mock_set_ca_cert.stop)
self.mock_set_client_cert = mock.patch.object(
prometheus_client.PrometheusAPIClient, 'set_client_cert').start()
self.addCleanup(self.mock_set_client_cert.stop)
self.mock_set_basic_auth = mock.patch.object(
prometheus_client.PrometheusAPIClient, 'set_basic_auth').start()
self.addCleanup(self.mock_set_basic_auth.stop)
self.mock_build_fqdn_labels = mock.patch.object(
prometheus_helper.PrometheusHelper,
'_build_prometheus_fqdn_labels').start()
self.addCleanup(self.mock_build_fqdn_labels.stop)
def test_unset_missing_prometheus_host(self):
cfg.CONF.prometheus_client.port = '123'
cfg.CONF.prometheus_client.host = None
self.assertRaisesRegex(
exception.MissingParameter, 'prometheus host and port must be '
'set in watcher.conf',
prometheus_helper.PrometheusHelper
)
cfg.CONF.prometheus_client.host = ''
self.assertRaisesRegex(
exception.MissingParameter, 'prometheus host and port must be '
'set in watcher.conf',
prometheus_helper.PrometheusHelper
)
def test_unset_missing_prometheus_port(self):
cfg.CONF.prometheus_client.host = 'some.host.domain'
cfg.CONF.prometheus_client.port = None
self.assertRaisesRegex(
exception.MissingParameter, 'prometheus host and port must be '
'set in watcher.conf',
prometheus_helper.PrometheusHelper
)
cfg.CONF.prometheus_client.port = ''
self.assertRaisesRegex(
exception.MissingParameter, 'prometheus host and port must be '
'set in watcher.conf',
prometheus_helper.PrometheusHelper
)
def test_invalid_prometheus_port(self):
cfg.CONF.prometheus_client.host = "hostOK"
cfg.CONF.prometheus_client.port = "123badPort"
self.assertRaisesRegex(
exception.InvalidParameter, "missing or invalid port number "
"'123badPort'",
prometheus_helper.PrometheusHelper
)
cfg.CONF.prometheus_client.port = "123456"
self.assertRaisesRegex(
exception.InvalidParameter, "missing or invalid port number "
"'123456'",
prometheus_helper.PrometheusHelper
)
def test_invalid_prometheus_host(self):
cfg.CONF.prometheus_client.port = "123"
cfg.CONF.prometheus_client.host = "-badhost"
self.assertRaisesRegex(
exception.InvalidParameter, "hostname '-badhost' "
"failed regex match",
prometheus_helper.PrometheusHelper
)
too_long_hostname = ("a" * 256)
cfg.CONF.prometheus_client.host = too_long_hostname
self.assertRaisesRegex(
exception.InvalidParameter, ("hostname is too long: " +
"'" + too_long_hostname + "'"),
prometheus_helper.PrometheusHelper
)
def test_get_fqdn_label(self):
fqdn = 'fqdn_label'
cfg.CONF.prometheus_client.fqdn_label = fqdn
self.assertEqual(
fqdn,
self.helper._get_fqdn_label()
)
def test_get_instance_uuid_label(self):
instance_uuid = 'instance_uuid_label'
cfg.CONF.prometheus_client.instance_uuid_label = instance_uuid
self.assertEqual(
instance_uuid,
self.helper._get_instance_uuid_label()
)
def test_setup_prometheus_client_no_auth_no_tls(self):
cfg.CONF.prometheus_client.host = "somehost"
cfg.CONF.prometheus_client.port = "1234"
prometheus_helper.PrometheusHelper()
self.mock_init.assert_called_once_with("somehost:1234")
self.mock_set_basic_auth.assert_not_called()
self.mock_set_client_cert.assert_not_called()
self.mock_set_ca_cert.assert_not_called()
def test_setup_prometheus_client_tls(self):
cfg.CONF.prometheus_client.cafile = "/some/path"
prometheus_helper.PrometheusHelper()
self.mock_set_ca_cert.assert_called_once_with("/some/path")
def test_setup_prometheus_client_basic_auth(self):
cfg.CONF.prometheus_client.username = "user"
cfg.CONF.prometheus_client.password = "password"
prometheus_helper.PrometheusHelper()
self.mock_set_basic_auth.assert_called_once_with("user", "password")
def test_setup_prometheus_client_mtls(self):
cfg.CONF.prometheus_client.certfile = "/cert/path"
cfg.CONF.prometheus_client.keyfile = "/key/path"
cfg.CONF.prometheus_client.cafile = "/ca/path"
prometheus_helper.PrometheusHelper()
self.mock_set_client_cert.assert_called_once_with(
"/cert/path", "/key/path")