Split get_prometheus_client to multiple functions

Improvements:
- better readability
- get_prom_client_from_keystone can be used by watcher for
  PrometheusAPIClient initialization.
- A better split between how the client is created for Aetos and
  how it's created for Prometheus.

Changes to the previous behavior of get_prometheus_client:
- It's not possible to override PrometheusAPIClient arguments with
  environment variables if they're retrieved from Keystone.
- It's not possible to use config file / environment variables to
  connect to Aetos (the keystone session parameter won't be passed to
  PrometheusAPIClient and so it won't be able to authenticate)

I think even though, there are some changes to how Aetos can access
can be configured, it's getting it a bit closer to how other services
work (getting the endpoint from keystone and not requiring / allowing
additional configuration). Prometheus access configuration through
config file / env variables stays unchanged.

Change-Id: Icd08347056e92502992d4fee799bb3e06e03c0c9
Signed-off-by: Jaromir Wysoglad <jwysogla@redhat.com>
This commit is contained in:
Jaromir Wysoglad
2025-07-24 13:42:58 -04:00
parent f1dd649a39
commit 2f31846d73
3 changed files with 69 additions and 67 deletions

View File

@@ -137,26 +137,6 @@ class GetPrometheusClientTest(testtools.TestCase):
) )
ca_m.assert_called_with(True) ca_m.assert_called_with(True)
def test_get_prometheus_client_from_keystone_custom_ca(self):
prom_endpoint = "https://localhost:1234/prometheus"
keystone_session = session.Session()
config_data = 'ca_cert: "ca/path"'
config_file = mock.mock_open(read_data=config_data)("name", 'r')
with mock.patch.dict(os.environ, {}), \
mock.patch.object(metric_utils, 'get_config_file',
return_value=config_file), \
mock.patch.object(adapter.Adapter, 'get_endpoint',
return_value=prom_endpoint), \
mock.patch.object(prometheus_client.PrometheusAPIClient,
"__init__", return_value=None) as init_m, \
mock.patch.object(prometheus_client.PrometheusAPIClient,
"set_ca_cert") as ca_m:
metric_utils.get_prometheus_client(keystone_session)
init_m.assert_called_with(
"localhost:1234", keystone_session, "prometheus"
)
ca_m.assert_called_with("ca/path")
def test_get_prometheus_client_from_env_vars_ipv6(self): def test_get_prometheus_client_from_env_vars_ipv6(self):
patched_env = {'PROMETHEUS_HOST': '2607:5300:201:2000::654', patched_env = {'PROMETHEUS_HOST': '2607:5300:201:2000::654',
'PROMETHEUS_PORT': '1234'} 'PROMETHEUS_PORT': '1234'}

View File

@@ -49,13 +49,33 @@ def get_config_file():
return None return None
def get_prometheus_client(session=None, adapter_options={}): def get_prom_client_from_keystone(session, adapter_options=None):
host = None if adapter_options is None:
port = None adapter_options = {}
ca_cert = None endpoint = adapter.Adapter(
root_path = "" session=session, **adapter_options
is_aetos = False ).get_endpoint()
parsed_url = parse.urlparse(endpoint)
escaped_host = netutils.escape_ipv6(parsed_url.hostname)
root_path = parsed_url.path.strip('/')
tls = parsed_url.scheme == "https"
if parsed_url.port is not None:
url = f'{escaped_host}:{parsed_url.port}'
else:
url = escaped_host
client = PrometheusAPIClient(url, session, root_path)
if tls:
client.set_ca_cert(True)
return client
def get_prom_client_from_file_or_env():
host = port = ca_cert = None
root_path = ''
conf_file = get_config_file() conf_file = get_config_file()
if conf_file is not None: if conf_file is not None:
conf = yaml.safe_load(conf_file) conf = yaml.safe_load(conf_file)
@@ -68,37 +88,6 @@ def get_prometheus_client(session=None, adapter_options={}):
if 'root_path' in conf: if 'root_path' in conf:
root_path = conf['root_path'] root_path = conf['root_path']
conf_file.close() conf_file.close()
if session is not None and (host is None or port is None):
try:
endpoint = adapter.Adapter(
session=session, **adapter_options
).get_endpoint()
parsed_url = parse.urlparse(endpoint)
host = parsed_url.hostname
root_path = parsed_url.path.strip('/')
if parsed_url.scheme == "https" and ca_cert is None:
# NOTE(jwysogla): Use the default CA certs if the scheme
# is https, but keep the original value if already set,
# so that a custom certificate can be set in the config
# file, while the endpoint is retrieved from keystone.
ca_cert = True
if parsed_url.port is not None:
port = parsed_url.port
elif parsed_url.scheme == "https":
port = 443
else:
port = 80
is_aetos = True
except keystone_exception.EndpointNotFound:
# NOTE(jwysogla): Don't do anything here. It's still possible
# to get the correct endpoint configuration from the env vars.
# If that doesn't work, the same error message is part of the
# exception raised below.
pass
# NOTE(jwysogla): We allow to overide the prometheus.yaml by
# the environment variables
if 'PROMETHEUS_HOST' in os.environ: if 'PROMETHEUS_HOST' in os.environ:
host = os.environ['PROMETHEUS_HOST'] host = os.environ['PROMETHEUS_HOST']
if 'PROMETHEUS_PORT' in os.environ: if 'PROMETHEUS_PORT' in os.environ:
@@ -109,22 +98,48 @@ def get_prometheus_client(session=None, adapter_options={}):
root_path = os.environ['PROMETHEUS_ROOT_PATH'] root_path = os.environ['PROMETHEUS_ROOT_PATH']
if host is None or port is None: if host is None or port is None:
raise ConfigurationError("Can't find prometheus host and " raise ConfigurationError("Can't find prometheus host and "
"port configuration and endpoint for " "port configuration in config file or "
"metric-storage not found.") "environment variables.")
escaped_host = netutils.escape_ipv6(host) escaped_host = netutils.escape_ipv6(host)
if is_aetos: client = PrometheusAPIClient(
client = PrometheusAPIClient( f"{escaped_host}:{port}", None, root_path
f"{escaped_host}:{port}", session, root_path )
)
else:
client = PrometheusAPIClient(
f"{escaped_host}:{port}", None, root_path
)
if ca_cert is not None: if ca_cert is not None:
client.set_ca_cert(ca_cert) client.set_ca_cert(ca_cert)
return client return client
def get_prometheus_client(session=None, adapter_options=None):
if adapter_options is None:
adapter_options = {}
keystone_error = None
if session is not None:
try:
return get_prom_client_from_keystone(session, adapter_options)
except keystone_exception.EndpointNotFound as e:
# NOTE(jwysogla): Fallback to get the endpoint configuration from
# the config file and env vars. If that doesn't work, the error
# message is raised as ConfigurationError later.
keystone_error = e
LOG.debug("Aetos endpoint discovery from Keystone failed: %s", e)
# NOTE(jwysogla): Always fallback to the original method of discovery
# through config file and env variables for backwards compatibility.
try:
return get_prom_client_from_file_or_env()
except ConfigurationError as e:
if keystone_error is not None:
raise ConfigurationError(
f"Failed to configure Prometheus client. "
f"Aetos discovery from keystone failed: '{keystone_error}'. "
f"Prometheus configuration from config file and environment "
f"variables failed: '{e}'"
)
else:
raise e
def get_client(obj): def get_client(obj):
return obj.app.client_manager.observabilityclient return obj.app.client_manager.observabilityclient

View File

@@ -0,0 +1,7 @@
---
upgrade:
- |
Aetos endpoint information discovered from keystone can no longer
be overridden by a config file or by environment variables. From
now on, Aetos access must be fully discoverable from Keystone
endpoints only.