Add automated unit testing and a set of tests (#9)
* Add unit testing * Fix code according to CI This includes: - formating changes - rewording of some doc strings - adding support to {label!~'value'} in rbac * Add unit tests automation * Fix CI automation * Add requirements.txt
This commit is contained in:
parent
037437e995
commit
53b335aaca
23
.github/workflows/unit_tests.yml
vendored
Normal file
23
.github/workflows/unit_tests.yml
vendored
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
name: unit_tests
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
test:
|
||||||
|
runs-on: ubuntu-22.04
|
||||||
|
timeout-minutes: 30
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
env:
|
||||||
|
- pep8
|
||||||
|
- py39
|
||||||
|
- py311
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
- name: Install dependencies
|
||||||
|
run: ./tools/install_deps.sh
|
||||||
|
- name: Run tox
|
||||||
|
run: tox -e ${{ matrix.env }}
|
@ -12,7 +12,7 @@
|
|||||||
# License for the specific language governing permissions and limitations
|
# License for the specific language governing permissions and limitations
|
||||||
# under the License.
|
# under the License.
|
||||||
|
|
||||||
"""OpenStackClient Plugin interface"""
|
"""OpenStackClient Plugin interface."""
|
||||||
|
|
||||||
from osc_lib import utils
|
from osc_lib import utils
|
||||||
|
|
||||||
@ -26,7 +26,7 @@ API_VERSIONS = {
|
|||||||
|
|
||||||
|
|
||||||
def make_client(instance):
|
def make_client(instance):
|
||||||
"""Returns a client to the ClientManager
|
"""Return a client to the ClientManager.
|
||||||
|
|
||||||
Called to instantiate the requested client version. instance has
|
Called to instantiate the requested client version. instance has
|
||||||
any available auth info that may be required to prepare the client.
|
any available auth info that may be required to prepare the client.
|
||||||
@ -47,7 +47,7 @@ def make_client(instance):
|
|||||||
|
|
||||||
|
|
||||||
def build_option_parser(parser):
|
def build_option_parser(parser):
|
||||||
"""Hook to add global options
|
"""Add global options.
|
||||||
|
|
||||||
Called from openstackclient.shell.OpenStackShell.__init__()
|
Called from openstackclient.shell.OpenStackShell.__init__()
|
||||||
after the builtin parser has been initialized. This is
|
after the builtin parser has been initialized. This is
|
||||||
|
@ -13,6 +13,7 @@
|
|||||||
# under the License.
|
# under the License.
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
import requests
|
import requests
|
||||||
|
|
||||||
|
|
||||||
@ -95,13 +96,12 @@ class PrometheusAPIClient:
|
|||||||
return decoded
|
return decoded
|
||||||
|
|
||||||
def query(self, query):
|
def query(self, query):
|
||||||
"""Sends custom queries to Prometheus
|
"""Send custom queries to Prometheus.
|
||||||
|
|
||||||
:param query: the query to send
|
:param query: the query to send
|
||||||
:type query: str
|
:type query: str
|
||||||
"""
|
"""
|
||||||
|
LOG.debug("Querying prometheus with query: %s", query)
|
||||||
LOG.debug(f"Querying prometheus with query: {query}")
|
|
||||||
decoded = self._get("query", dict(query=query))
|
decoded = self._get("query", dict(query=query))
|
||||||
|
|
||||||
if decoded['data']['resultType'] == 'vector':
|
if decoded['data']['resultType'] == 'vector':
|
||||||
@ -111,38 +111,35 @@ class PrometheusAPIClient:
|
|||||||
return result
|
return result
|
||||||
|
|
||||||
def series(self, matches):
|
def series(self, matches):
|
||||||
"""Queries the /series/ endpoint of prometheus
|
"""Query the /series/ endpoint of prometheus.
|
||||||
|
|
||||||
:param matches: List of matches to send as parameters
|
:param matches: List of matches to send as parameters
|
||||||
:type matches: [str]
|
:type matches: [str]
|
||||||
"""
|
"""
|
||||||
|
LOG.debug("Querying prometheus for series with matches: %s", matches)
|
||||||
LOG.debug(f"Querying prometheus for series with matches: {matches}")
|
|
||||||
decoded = self._get("series", {"match[]": matches})
|
decoded = self._get("series", {"match[]": matches})
|
||||||
|
|
||||||
return decoded['data']
|
return decoded['data']
|
||||||
|
|
||||||
def labels(self):
|
def labels(self):
|
||||||
"""Queries the /labels/ endpoint of prometheus, returns list of labels
|
"""Query the /labels/ endpoint of prometheus, returns list of labels.
|
||||||
|
|
||||||
There isn't a way to tell prometheus to restrict
|
There isn't a way to tell prometheus to restrict
|
||||||
which labels to return. It's not possible to enforce
|
which labels to return. It's not possible to enforce
|
||||||
rbac with this for example.
|
rbac with this for example.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
LOG.debug("Querying prometheus for labels")
|
LOG.debug("Querying prometheus for labels")
|
||||||
decoded = self._get("labels")
|
decoded = self._get("labels")
|
||||||
|
|
||||||
return decoded['data']
|
return decoded['data']
|
||||||
|
|
||||||
def label_values(self, label):
|
def label_values(self, label):
|
||||||
"""Queries prometheus for values of a specified label.
|
"""Query prometheus for values of a specified label.
|
||||||
|
|
||||||
:param label: Name of label for which to return values
|
:param label: Name of label for which to return values
|
||||||
:type label: str
|
:type label: str
|
||||||
"""
|
"""
|
||||||
|
LOG.debug("Querying prometheus for the values of label: %s", label)
|
||||||
LOG.debug(f"Querying prometheus for the values of label: {label}")
|
|
||||||
decoded = self._get(f"label/{label}/values")
|
decoded = self._get(f"label/{label}/values")
|
||||||
|
|
||||||
return decoded['data']
|
return decoded['data']
|
||||||
@ -152,7 +149,7 @@ class PrometheusAPIClient:
|
|||||||
# ---------
|
# ---------
|
||||||
|
|
||||||
def delete(self, matches, start=None, end=None):
|
def delete(self, matches, start=None, end=None):
|
||||||
"""Deletes some metrics from prometheus
|
"""Delete some metrics from prometheus.
|
||||||
|
|
||||||
:param matches: List of matches, that specify which metrics to delete
|
:param matches: List of matches, that specify which metrics to delete
|
||||||
:type matches [str]
|
:type matches [str]
|
||||||
@ -168,8 +165,7 @@ class PrometheusAPIClient:
|
|||||||
# way to know if anything got actually deleted.
|
# way to know if anything got actually deleted.
|
||||||
# It does however return 500 code and error msg
|
# It does however return 500 code and error msg
|
||||||
# if the admin APIs are disabled.
|
# if the admin APIs are disabled.
|
||||||
|
LOG.debug("Deleting metrics from prometheus matching: %s", matches)
|
||||||
LOG.debug(f"Deleting metrics from prometheus matching: {matches}")
|
|
||||||
try:
|
try:
|
||||||
self._post("admin/tsdb/delete_series", {"match[]": matches,
|
self._post("admin/tsdb/delete_series", {"match[]": matches,
|
||||||
"start": start,
|
"start": start,
|
||||||
@ -181,8 +177,7 @@ class PrometheusAPIClient:
|
|||||||
raise exc
|
raise exc
|
||||||
|
|
||||||
def clean_tombstones(self):
|
def clean_tombstones(self):
|
||||||
"""Asks prometheus to clean tombstones"""
|
"""Ask prometheus to clean tombstones."""
|
||||||
|
|
||||||
LOG.debug("Cleaning tombstones from prometheus")
|
LOG.debug("Cleaning tombstones from prometheus")
|
||||||
try:
|
try:
|
||||||
self._post("admin/tsdb/clean_tombstones")
|
self._post("admin/tsdb/clean_tombstones")
|
||||||
@ -193,8 +188,7 @@ class PrometheusAPIClient:
|
|||||||
raise exc
|
raise exc
|
||||||
|
|
||||||
def snapshot(self):
|
def snapshot(self):
|
||||||
"""Creates a snapshot and returns the file name containing the data"""
|
"""Create a snapshot and return the file name containing the data."""
|
||||||
|
|
||||||
LOG.debug("Taking prometheus data snapshot")
|
LOG.debug("Taking prometheus data snapshot")
|
||||||
ret = self._post("admin/tsdb/snapshot")
|
ret = self._post("admin/tsdb/snapshot")
|
||||||
return ret["data"]["name"]
|
return ret["data"]["name"]
|
||||||
|
0
observabilityclient/tests/unit/__init__.py
Normal file
0
observabilityclient/tests/unit/__init__.py
Normal file
142
observabilityclient/tests/unit/test_cli.py
Normal file
142
observabilityclient/tests/unit/test_cli.py
Normal file
@ -0,0 +1,142 @@
|
|||||||
|
# Copyright 2023 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
|
||||||
|
|
||||||
|
import testtools
|
||||||
|
|
||||||
|
from observabilityclient.prometheus_client import PrometheusMetric
|
||||||
|
from observabilityclient.utils import metric_utils
|
||||||
|
from observabilityclient.v1 import cli
|
||||||
|
|
||||||
|
|
||||||
|
class CliTest(testtools.TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
super(CliTest, self).setUp()
|
||||||
|
self.client = mock.Mock()
|
||||||
|
self.client.query = mock.Mock()
|
||||||
|
|
||||||
|
def test_list(self):
|
||||||
|
args_enabled = {'disable_rbac': False}
|
||||||
|
args_disabled = {'disable_rbac': True}
|
||||||
|
|
||||||
|
metric_names = ['name1', 'name2', 'name3']
|
||||||
|
expected = (['metric_name'], [['name1'], ['name2'], ['name3']])
|
||||||
|
cli_list = cli.List(mock.Mock(), mock.Mock())
|
||||||
|
|
||||||
|
with (mock.patch.object(metric_utils, 'get_client',
|
||||||
|
return_value=self.client),
|
||||||
|
mock.patch.object(self.client.query, 'list',
|
||||||
|
return_value=metric_names) as m):
|
||||||
|
ret1 = cli_list.take_action(args_enabled)
|
||||||
|
m.assert_called_with(disable_rbac=False)
|
||||||
|
|
||||||
|
ret2 = cli_list.take_action(args_disabled)
|
||||||
|
m.assert_called_with(disable_rbac=True)
|
||||||
|
|
||||||
|
self.assertEqual(ret1, expected)
|
||||||
|
self.assertEqual(ret2, expected)
|
||||||
|
|
||||||
|
def test_show(self):
|
||||||
|
args_enabled = {'name': 'metric_name', 'disable_rbac': False}
|
||||||
|
args_disabled = {'name': 'metric_name', 'disable_rbac': True}
|
||||||
|
|
||||||
|
metric = {
|
||||||
|
'value': [123456, 12],
|
||||||
|
'metric': {'label1': 'value1'}
|
||||||
|
}
|
||||||
|
prom_metric = [PrometheusMetric(metric)]
|
||||||
|
expected = ['label1', 'value'], [['value1', 12]]
|
||||||
|
|
||||||
|
cli_show = cli.Show(mock.Mock(), mock.Mock())
|
||||||
|
|
||||||
|
with (mock.patch.object(metric_utils, 'get_client',
|
||||||
|
return_value=self.client),
|
||||||
|
mock.patch.object(self.client.query, 'show',
|
||||||
|
return_value=prom_metric) as m):
|
||||||
|
|
||||||
|
ret1 = cli_show.take_action(args_enabled)
|
||||||
|
m.assert_called_with('metric_name', disable_rbac=False)
|
||||||
|
|
||||||
|
ret2 = cli_show.take_action(args_disabled)
|
||||||
|
m.assert_called_with('metric_name', disable_rbac=True)
|
||||||
|
|
||||||
|
self.assertEqual(ret1, expected)
|
||||||
|
self.assertEqual(ret2, expected)
|
||||||
|
|
||||||
|
def test_query(self):
|
||||||
|
query = ("some_query{label!~'not_this_value'} - "
|
||||||
|
"sum(second_metric{label='this'})")
|
||||||
|
args_enabled = {'query': query, 'disable_rbac': False}
|
||||||
|
args_disabled = {'query': query, 'disable_rbac': True}
|
||||||
|
|
||||||
|
metric = {
|
||||||
|
'value': [123456, 12],
|
||||||
|
'metric': {'label1': 'value1'}
|
||||||
|
}
|
||||||
|
|
||||||
|
prom_metric = [PrometheusMetric(metric)]
|
||||||
|
expected = ['label1', 'value'], [['value1', 12]]
|
||||||
|
|
||||||
|
cli_query = cli.Query(mock.Mock(), mock.Mock())
|
||||||
|
|
||||||
|
with (mock.patch.object(metric_utils, 'get_client',
|
||||||
|
return_value=self.client),
|
||||||
|
mock.patch.object(self.client.query, 'query',
|
||||||
|
return_value=prom_metric) as m):
|
||||||
|
|
||||||
|
ret1 = cli_query.take_action(args_enabled)
|
||||||
|
m.assert_called_with(query, disable_rbac=False)
|
||||||
|
|
||||||
|
ret2 = cli_query.take_action(args_disabled)
|
||||||
|
m.assert_called_with(query, disable_rbac=True)
|
||||||
|
|
||||||
|
self.assertEqual(ret1, expected)
|
||||||
|
self.assertEqual(ret2, expected)
|
||||||
|
|
||||||
|
def test_delete(self):
|
||||||
|
matches = "some_label_name"
|
||||||
|
args = {'matches': matches, 'start': 0, 'end': 10}
|
||||||
|
|
||||||
|
cli_delete = cli.Delete(mock.Mock(), mock.Mock())
|
||||||
|
|
||||||
|
with (mock.patch.object(metric_utils, 'get_client',
|
||||||
|
return_value=self.client),
|
||||||
|
mock.patch.object(self.client.query, 'delete') as m):
|
||||||
|
|
||||||
|
cli_delete.take_action(args)
|
||||||
|
m.assert_called_with(matches, 0, 10)
|
||||||
|
|
||||||
|
def test_clean_combstones(self):
|
||||||
|
cli_clean_tombstones = cli.CleanTombstones(mock.Mock(), mock.Mock())
|
||||||
|
|
||||||
|
with (mock.patch.object(metric_utils, 'get_client',
|
||||||
|
return_value=self.client),
|
||||||
|
mock.patch.object(self.client.query, 'clean_tombstones') as m):
|
||||||
|
|
||||||
|
cli_clean_tombstones.take_action({})
|
||||||
|
m.assert_called_once()
|
||||||
|
|
||||||
|
def test_snapshot(self):
|
||||||
|
cli_snapshot = cli.Snapshot(mock.Mock(), mock.Mock())
|
||||||
|
file_name = 'some_file_name'
|
||||||
|
|
||||||
|
with (mock.patch.object(metric_utils, 'get_client',
|
||||||
|
return_value=self.client),
|
||||||
|
mock.patch.object(self.client.query, 'snapshot',
|
||||||
|
return_value=file_name) as m):
|
||||||
|
|
||||||
|
ret = cli_snapshot.take_action({})
|
||||||
|
m.assert_called_once()
|
||||||
|
self.assertEqual(ret, (["Snapshot file name"], [[file_name]]))
|
515
observabilityclient/tests/unit/test_prometheus_client.py
Normal file
515
observabilityclient/tests/unit/test_prometheus_client.py
Normal file
@ -0,0 +1,515 @@
|
|||||||
|
# Copyright 2023 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
|
||||||
|
|
||||||
|
import requests
|
||||||
|
|
||||||
|
import testtools
|
||||||
|
|
||||||
|
from observabilityclient import prometheus_client as client
|
||||||
|
|
||||||
|
|
||||||
|
class MetricListMatcher(testtools.Matcher):
|
||||||
|
def __init__(self, expected):
|
||||||
|
self.expected = expected
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return ("Matches Lists of metrics as returned "
|
||||||
|
"by prometheus_client.PremetheusAPIClient.query")
|
||||||
|
|
||||||
|
def metric_to_str(self, metric):
|
||||||
|
return (f"Labels: {metric.labels}\n"
|
||||||
|
f"Timestamp: {metric.timestamp}\n"
|
||||||
|
f"Value: {metric.value}")
|
||||||
|
|
||||||
|
def match(self, observed):
|
||||||
|
if len(self.expected) != len(observed):
|
||||||
|
description = (f"len(expected) != len(observed) because "
|
||||||
|
f"{len(self.expected)} != {len(observed)}")
|
||||||
|
return testtools.matchers.Mismatch(description=description)
|
||||||
|
|
||||||
|
for e in self.expected:
|
||||||
|
for o in observed:
|
||||||
|
if (e.timestamp == o.timestamp and
|
||||||
|
e.value == o.value and
|
||||||
|
e.labels == o.labels):
|
||||||
|
observed.remove(o)
|
||||||
|
break
|
||||||
|
|
||||||
|
if len(observed) != 0:
|
||||||
|
description = "Couldn't match the following metrics:\n"
|
||||||
|
for o in observed:
|
||||||
|
description += self.metric_to_str(o) + "\n\n"
|
||||||
|
return testtools.matchers.Mismatch(description=description)
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
class PrometheusAPIClientTestBase(testtools.TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
super(PrometheusAPIClientTestBase, self).setUp()
|
||||||
|
|
||||||
|
class GoodResponse(object):
|
||||||
|
def __init__(self):
|
||||||
|
self.status_code = 200
|
||||||
|
|
||||||
|
def json(self):
|
||||||
|
return {"status": "success"}
|
||||||
|
|
||||||
|
class BadResponse(object):
|
||||||
|
def __init__(self):
|
||||||
|
self.status_code = 500
|
||||||
|
|
||||||
|
def json(self):
|
||||||
|
return {"status": "error", "error": "test_error"}
|
||||||
|
|
||||||
|
class NoContentResponse(object):
|
||||||
|
def __init__(self):
|
||||||
|
self.status_code = 204
|
||||||
|
|
||||||
|
def json(self):
|
||||||
|
raise requests.exceptions.JSONDecodeError("No content")
|
||||||
|
|
||||||
|
|
||||||
|
class PrometheusAPIClientTest(PrometheusAPIClientTestBase):
|
||||||
|
def test_get(self):
|
||||||
|
url = "test"
|
||||||
|
expected_url = "http://localhost:9090/api/v1/test"
|
||||||
|
|
||||||
|
params = {"query": "ceilometer_image_size{publisher='localhost'}"}
|
||||||
|
expected_params = params
|
||||||
|
|
||||||
|
return_value = self.GoodResponse()
|
||||||
|
with mock.patch.object(requests.Session, 'get',
|
||||||
|
return_value=return_value) as m:
|
||||||
|
c = client.PrometheusAPIClient("localhost:9090")
|
||||||
|
c._get(url, params)
|
||||||
|
|
||||||
|
m.assert_called_with(expected_url,
|
||||||
|
params=expected_params,
|
||||||
|
headers={'Accept': 'application/json'})
|
||||||
|
|
||||||
|
def test_get_error(self):
|
||||||
|
url = "test"
|
||||||
|
params = {"query": "ceilometer_image_size{publisher='localhost'}"}
|
||||||
|
|
||||||
|
return_value = self.BadResponse()
|
||||||
|
with mock.patch.object(requests.Session, 'get',
|
||||||
|
return_value=return_value):
|
||||||
|
c = client.PrometheusAPIClient("localhost:9090")
|
||||||
|
self.assertRaises(client.PrometheusAPIClientError,
|
||||||
|
c._get, url, params)
|
||||||
|
|
||||||
|
return_value = self.NoContentResponse()
|
||||||
|
with mock.patch.object(requests.Session, 'get',
|
||||||
|
return_value=return_value):
|
||||||
|
c = client.PrometheusAPIClient("localhost:9090")
|
||||||
|
self.assertRaises(client.PrometheusAPIClientError,
|
||||||
|
c._get, url, params)
|
||||||
|
|
||||||
|
def test_post(self):
|
||||||
|
url = "test"
|
||||||
|
expected_url = "http://localhost:9090/api/v1/test"
|
||||||
|
|
||||||
|
params = {"query": "ceilometer_image_size{publisher='localhost'}"}
|
||||||
|
expected_params = params
|
||||||
|
|
||||||
|
return_value = self.GoodResponse()
|
||||||
|
with mock.patch.object(requests.Session, 'post',
|
||||||
|
return_value=return_value) as m:
|
||||||
|
c = client.PrometheusAPIClient("localhost:9090")
|
||||||
|
c._post(url, params)
|
||||||
|
|
||||||
|
m.assert_called_with(expected_url,
|
||||||
|
params=expected_params,
|
||||||
|
headers={'Accept': 'application/json'})
|
||||||
|
|
||||||
|
def test_post_error(self):
|
||||||
|
url = "test"
|
||||||
|
params = {"query": "ceilometer_image_size{publisher='localhost'}"}
|
||||||
|
|
||||||
|
return_value = self.BadResponse()
|
||||||
|
with mock.patch.object(requests.Session, 'post',
|
||||||
|
return_value=return_value):
|
||||||
|
c = client.PrometheusAPIClient("localhost:9090")
|
||||||
|
self.assertRaises(client.PrometheusAPIClientError,
|
||||||
|
c._post, url, params)
|
||||||
|
|
||||||
|
return_value = self.NoContentResponse()
|
||||||
|
with mock.patch.object(requests.Session, 'post',
|
||||||
|
return_value=return_value):
|
||||||
|
c = client.PrometheusAPIClient("localhost:9090")
|
||||||
|
self.assertRaises(client.PrometheusAPIClientError,
|
||||||
|
c._post, url, params)
|
||||||
|
|
||||||
|
|
||||||
|
class PrometheusAPIClientQueryTest(PrometheusAPIClientTestBase):
|
||||||
|
def setUp(self):
|
||||||
|
super().setUp()
|
||||||
|
|
||||||
|
class GoodQueryResponse(PrometheusAPIClientTestBase.GoodResponse):
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__()
|
||||||
|
self.result1 = {
|
||||||
|
"metric": {
|
||||||
|
"__name__": "test1",
|
||||||
|
},
|
||||||
|
"value": [103254, "1"]
|
||||||
|
}
|
||||||
|
self.result2 = {
|
||||||
|
"metric": {
|
||||||
|
"__name__": "test2",
|
||||||
|
},
|
||||||
|
"value": [103255, "2"]
|
||||||
|
}
|
||||||
|
self.expected = [client.PrometheusMetric(self.result1),
|
||||||
|
client.PrometheusMetric(self.result2)]
|
||||||
|
|
||||||
|
def json(self):
|
||||||
|
return {
|
||||||
|
"status": "success",
|
||||||
|
"data": {
|
||||||
|
"resultType": "vector",
|
||||||
|
"result": [self.result1, self.result2]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class EmptyQueryResponse(PrometheusAPIClientTestBase.GoodResponse):
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__()
|
||||||
|
self.expected = []
|
||||||
|
|
||||||
|
def json(self):
|
||||||
|
return {
|
||||||
|
"status": "success",
|
||||||
|
"data": {
|
||||||
|
"resultType": "vector",
|
||||||
|
"result": []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def test_query(self):
|
||||||
|
query = "ceilometer_image_size{publisher='localhost.localdomain'}"
|
||||||
|
|
||||||
|
matcher = MetricListMatcher(self.GoodQueryResponse().expected)
|
||||||
|
return_value = self.GoodQueryResponse().json()
|
||||||
|
with mock.patch.object(client.PrometheusAPIClient, '_get',
|
||||||
|
return_value=return_value) as m:
|
||||||
|
c = client.PrometheusAPIClient("localhost:9090")
|
||||||
|
ret = c.query(query)
|
||||||
|
|
||||||
|
m.assert_called_with("query", {"query": query})
|
||||||
|
self.assertThat(ret, matcher)
|
||||||
|
|
||||||
|
return_value = self.EmptyQueryResponse().json()
|
||||||
|
with mock.patch.object(client.PrometheusAPIClient, '_get',
|
||||||
|
return_value=return_value) as m:
|
||||||
|
c = client.PrometheusAPIClient("localhost:9090")
|
||||||
|
ret = c.query(query)
|
||||||
|
|
||||||
|
self.assertEqual(self.EmptyQueryResponse().expected, ret)
|
||||||
|
|
||||||
|
def test_query_error(self):
|
||||||
|
query = "ceilometer_image_size{publisher='localhost.localdomain'}"
|
||||||
|
client_exception = client.PrometheusAPIClientError(self.BadResponse())
|
||||||
|
|
||||||
|
with mock.patch.object(client.PrometheusAPIClient, '_get',
|
||||||
|
side_effect=client_exception):
|
||||||
|
c = client.PrometheusAPIClient("localhost:9090")
|
||||||
|
|
||||||
|
self.assertRaises(client.PrometheusAPIClientError, c.query, query)
|
||||||
|
|
||||||
|
|
||||||
|
class PrometheusAPIClientSeriesTest(PrometheusAPIClientTestBase):
|
||||||
|
def setUp(self):
|
||||||
|
super().setUp()
|
||||||
|
|
||||||
|
class GoodSeriesResponse(PrometheusAPIClientTestBase.GoodResponse):
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__()
|
||||||
|
self.data = [{
|
||||||
|
"__name__": "up",
|
||||||
|
"job": "prometheus",
|
||||||
|
"instance": "localhost:9090"
|
||||||
|
}, {
|
||||||
|
"__name__": "up",
|
||||||
|
"job": "node",
|
||||||
|
"instance": "localhost:9091"
|
||||||
|
}, {
|
||||||
|
"__name__": "process_start_time_seconds",
|
||||||
|
"job": "prometheus",
|
||||||
|
"instance": "localhost:9090"
|
||||||
|
}]
|
||||||
|
self.expected = self.data
|
||||||
|
|
||||||
|
def json(self):
|
||||||
|
return {
|
||||||
|
"status": "success",
|
||||||
|
"data": self.data
|
||||||
|
}
|
||||||
|
|
||||||
|
class EmptySeriesResponse(PrometheusAPIClientTestBase.GoodResponse):
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__()
|
||||||
|
self.data = []
|
||||||
|
self.expected = self.data
|
||||||
|
|
||||||
|
def json(self):
|
||||||
|
return {
|
||||||
|
"status": "success",
|
||||||
|
"data": self.data
|
||||||
|
}
|
||||||
|
|
||||||
|
def test_series(self):
|
||||||
|
matches = ["up", "ceilometer_image_size"]
|
||||||
|
|
||||||
|
return_value = self.GoodSeriesResponse().json()
|
||||||
|
with mock.patch.object(client.PrometheusAPIClient, '_get',
|
||||||
|
return_value=return_value) as m:
|
||||||
|
c = client.PrometheusAPIClient("localhost:9090")
|
||||||
|
ret = c.series(matches)
|
||||||
|
|
||||||
|
m.assert_called_with("series", {"match[]": matches})
|
||||||
|
self.assertEqual(ret, self.GoodSeriesResponse().data)
|
||||||
|
|
||||||
|
return_value = self.EmptySeriesResponse().json()
|
||||||
|
with mock.patch.object(client.PrometheusAPIClient, '_get',
|
||||||
|
return_value=return_value) as m:
|
||||||
|
c = client.PrometheusAPIClient("localhost:9090")
|
||||||
|
ret = c.series(matches)
|
||||||
|
|
||||||
|
m.assert_called_with("series", {"match[]": matches})
|
||||||
|
self.assertEqual(ret, self.EmptySeriesResponse().data)
|
||||||
|
|
||||||
|
def test_series_error(self):
|
||||||
|
matches = ["up", "ceilometer_image_size"]
|
||||||
|
client_exception = client.PrometheusAPIClientError(self.BadResponse())
|
||||||
|
|
||||||
|
with mock.patch.object(client.PrometheusAPIClient, '_get',
|
||||||
|
side_effect=client_exception):
|
||||||
|
c = client.PrometheusAPIClient("localhost:9090")
|
||||||
|
|
||||||
|
self.assertRaises(client.PrometheusAPIClientError,
|
||||||
|
c.series,
|
||||||
|
matches)
|
||||||
|
|
||||||
|
|
||||||
|
class PrometheusAPIClientLabelsTest(PrometheusAPIClientTestBase):
|
||||||
|
def setUp(self):
|
||||||
|
super().setUp()
|
||||||
|
|
||||||
|
class GoodLabelsResponse(PrometheusAPIClientTestBase.GoodResponse):
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__()
|
||||||
|
self.labels = ["up", "job", "project_id"]
|
||||||
|
|
||||||
|
def json(self):
|
||||||
|
return {
|
||||||
|
"status": "success",
|
||||||
|
"data": self.labels
|
||||||
|
}
|
||||||
|
|
||||||
|
def test_labels(self):
|
||||||
|
return_value = self.GoodLabelsResponse().json()
|
||||||
|
with mock.patch.object(client.PrometheusAPIClient, '_get',
|
||||||
|
return_value=return_value) as m:
|
||||||
|
c = client.PrometheusAPIClient("localhost:9090")
|
||||||
|
ret = c.labels()
|
||||||
|
|
||||||
|
m.assert_called_with("labels")
|
||||||
|
self.assertEqual(ret, self.GoodLabelsResponse().labels)
|
||||||
|
|
||||||
|
def test_labels_error(self):
|
||||||
|
client_exception = client.PrometheusAPIClientError(self.BadResponse())
|
||||||
|
with mock.patch.object(client.PrometheusAPIClient, '_get',
|
||||||
|
side_effect=client_exception):
|
||||||
|
c = client.PrometheusAPIClient("localhost:9090")
|
||||||
|
|
||||||
|
self.assertRaises(client.PrometheusAPIClientError, c.labels)
|
||||||
|
|
||||||
|
|
||||||
|
class PrometheusAPIClientLabelValuesTest(PrometheusAPIClientTestBase):
|
||||||
|
def setUp(self):
|
||||||
|
super().setUp()
|
||||||
|
|
||||||
|
class GoodLabelValuesResponse(PrometheusAPIClientTestBase.GoodResponse):
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__()
|
||||||
|
self.values = ["prometheus", "some_other_value"]
|
||||||
|
|
||||||
|
def json(self):
|
||||||
|
return {
|
||||||
|
"status": "success",
|
||||||
|
"data": self.values
|
||||||
|
}
|
||||||
|
|
||||||
|
class EmptyLabelValuesResponse(PrometheusAPIClientTestBase.GoodResponse):
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__()
|
||||||
|
self.values = []
|
||||||
|
|
||||||
|
def json(self):
|
||||||
|
return {
|
||||||
|
"status": "success",
|
||||||
|
"data": self.values
|
||||||
|
}
|
||||||
|
|
||||||
|
def test_label_values(self):
|
||||||
|
label_name = "job"
|
||||||
|
|
||||||
|
return_value = self.GoodLabelValuesResponse().json()
|
||||||
|
with mock.patch.object(client.PrometheusAPIClient, '_get',
|
||||||
|
return_value=return_value) as m:
|
||||||
|
c = client.PrometheusAPIClient("localhost:9090")
|
||||||
|
ret = c.label_values(label_name)
|
||||||
|
|
||||||
|
m.assert_called_with(f"label/{label_name}/values")
|
||||||
|
self.assertEqual(ret, self.GoodLabelValuesResponse().values)
|
||||||
|
|
||||||
|
return_value = self.EmptyLabelValuesResponse().json()
|
||||||
|
with mock.patch.object(client.PrometheusAPIClient, '_get',
|
||||||
|
return_value=return_value) as m:
|
||||||
|
c = client.PrometheusAPIClient("localhost:9090")
|
||||||
|
ret = c.label_values(label_name)
|
||||||
|
|
||||||
|
m.assert_called_with(f"label/{label_name}/values")
|
||||||
|
self.assertEqual(ret, self.EmptyLabelValuesResponse().values)
|
||||||
|
|
||||||
|
def test_label_values_error(self):
|
||||||
|
label_name = "job"
|
||||||
|
client_exception = client.PrometheusAPIClientError(self.BadResponse())
|
||||||
|
|
||||||
|
with mock.patch.object(client.PrometheusAPIClient, '_get',
|
||||||
|
side_effect=client_exception):
|
||||||
|
c = client.PrometheusAPIClient("localhost:9090")
|
||||||
|
|
||||||
|
self.assertRaises(client.PrometheusAPIClientError,
|
||||||
|
c.label_values,
|
||||||
|
label_name)
|
||||||
|
|
||||||
|
|
||||||
|
class PrometheusAPIClientDeleteTest(PrometheusAPIClientTestBase):
|
||||||
|
def setUp(self):
|
||||||
|
super().setUp()
|
||||||
|
|
||||||
|
class GoodDeleteResponse(PrometheusAPIClientTestBase.NoContentResponse):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def test_delete(self):
|
||||||
|
matches = ["{job='prometheus'}", "up"]
|
||||||
|
start = 1
|
||||||
|
end = 12
|
||||||
|
resp = self.GoodDeleteResponse()
|
||||||
|
post_exception = client.PrometheusAPIClientError(resp)
|
||||||
|
|
||||||
|
with mock.patch.object(client.PrometheusAPIClient, '_post',
|
||||||
|
side_effect=post_exception) as m:
|
||||||
|
c = client.PrometheusAPIClient("localhost:9090")
|
||||||
|
# _post is expected to raise an exception. It's expected
|
||||||
|
# that the exception is caught inside delete. This
|
||||||
|
# test should run without exception getting out of delete
|
||||||
|
try:
|
||||||
|
c.delete(matches, start, end)
|
||||||
|
except Exception as ex: # noqa: B902
|
||||||
|
self.fail("Exception risen by delete: " + ex)
|
||||||
|
|
||||||
|
m.assert_called_with("admin/tsdb/delete_series",
|
||||||
|
{"match[]": matches,
|
||||||
|
"start": start,
|
||||||
|
"end": end})
|
||||||
|
|
||||||
|
def test_delete_error(self):
|
||||||
|
matches = ["{job='prometheus'}", "up"]
|
||||||
|
client_exception = client.PrometheusAPIClientError(self.BadResponse())
|
||||||
|
|
||||||
|
with mock.patch.object(client.PrometheusAPIClient, '_post',
|
||||||
|
side_effect=client_exception):
|
||||||
|
c = client.PrometheusAPIClient("localhost:9090")
|
||||||
|
|
||||||
|
self.assertRaises(client.PrometheusAPIClientError,
|
||||||
|
c.delete,
|
||||||
|
matches)
|
||||||
|
|
||||||
|
|
||||||
|
class PrometheusAPIClientCleanTombstonesTest(PrometheusAPIClientTestBase):
|
||||||
|
def setUp(self):
|
||||||
|
super().setUp()
|
||||||
|
|
||||||
|
class GoodCleanTombResponse(PrometheusAPIClientTestBase.NoContentResponse):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def test_clean_tombstones(self):
|
||||||
|
resp = self.GoodCleanTombResponse()
|
||||||
|
post_exception = client.PrometheusAPIClientError(resp)
|
||||||
|
|
||||||
|
with mock.patch.object(client.PrometheusAPIClient, '_post',
|
||||||
|
side_effect=post_exception) as m:
|
||||||
|
c = client.PrometheusAPIClient("localhost:9090")
|
||||||
|
# _post is expected to raise an exception. It's expected
|
||||||
|
# that the exception is caught inside clean_tombstones. This
|
||||||
|
# test should run without exception getting out of clean_tombstones
|
||||||
|
try:
|
||||||
|
c.clean_tombstones()
|
||||||
|
except Exception as ex: # noqa: B902
|
||||||
|
self.fail("Exception risen by clean_tombstones: " + ex)
|
||||||
|
|
||||||
|
m.assert_called_with("admin/tsdb/clean_tombstones")
|
||||||
|
|
||||||
|
def test_snapshot_error(self):
|
||||||
|
client_exception = client.PrometheusAPIClientError(self.BadResponse())
|
||||||
|
|
||||||
|
with mock.patch.object(client.PrometheusAPIClient, '_post',
|
||||||
|
side_effect=client_exception):
|
||||||
|
c = client.PrometheusAPIClient("localhost:9090")
|
||||||
|
|
||||||
|
self.assertRaises(client.PrometheusAPIClientError,
|
||||||
|
c.clean_tombstones)
|
||||||
|
|
||||||
|
|
||||||
|
class PrometheusAPIClientSnapshotTest(PrometheusAPIClientTestBase):
|
||||||
|
def setUp(self):
|
||||||
|
super().setUp()
|
||||||
|
|
||||||
|
class GoodSnapshotResponse(PrometheusAPIClientTestBase.NoContentResponse):
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__()
|
||||||
|
self.filename = "somefilename"
|
||||||
|
|
||||||
|
def json(self):
|
||||||
|
return {
|
||||||
|
"status": "success",
|
||||||
|
"data": {
|
||||||
|
"name": self.filename
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def test_snapshot(self):
|
||||||
|
return_value = self.GoodSnapshotResponse().json()
|
||||||
|
with mock.patch.object(client.PrometheusAPIClient, '_post',
|
||||||
|
return_value=return_value) as m:
|
||||||
|
c = client.PrometheusAPIClient("localhost:9090")
|
||||||
|
ret = c.snapshot()
|
||||||
|
|
||||||
|
m.assert_called_with("admin/tsdb/snapshot")
|
||||||
|
self.assertEqual(ret, self.GoodSnapshotResponse().filename)
|
||||||
|
|
||||||
|
def test_snapshot_error(self):
|
||||||
|
client_exception = client.PrometheusAPIClientError(self.BadResponse())
|
||||||
|
|
||||||
|
with mock.patch.object(client.PrometheusAPIClient, '_post',
|
||||||
|
side_effect=client_exception):
|
||||||
|
c = client.PrometheusAPIClient("localhost:9090")
|
||||||
|
|
||||||
|
self.assertRaises(client.PrometheusAPIClientError,
|
||||||
|
c.snapshot)
|
126
observabilityclient/tests/unit/test_python_api.py
Normal file
126
observabilityclient/tests/unit/test_python_api.py
Normal file
@ -0,0 +1,126 @@
|
|||||||
|
# Copyright 2023 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
|
||||||
|
|
||||||
|
import testtools
|
||||||
|
|
||||||
|
from observabilityclient import prometheus_client
|
||||||
|
from observabilityclient.tests.unit.test_prometheus_client import (
|
||||||
|
MetricListMatcher
|
||||||
|
)
|
||||||
|
from observabilityclient.v1 import python_api
|
||||||
|
from observabilityclient.v1 import rbac
|
||||||
|
|
||||||
|
|
||||||
|
class QueryManagerTest(testtools.TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
super(QueryManagerTest, self).setUp()
|
||||||
|
self.client = mock.Mock()
|
||||||
|
prom_client = prometheus_client.PrometheusAPIClient("somehost")
|
||||||
|
self.client.prometheus_client = prom_client
|
||||||
|
|
||||||
|
self.rbac = mock.Mock(wraps=rbac.Rbac(self.client, mock.Mock()))
|
||||||
|
self.rbac.default_labels = {'project': 'project_id'}
|
||||||
|
self.rbac.rbac_init_succesful = True
|
||||||
|
|
||||||
|
self.manager = python_api.QueryManager(self.client)
|
||||||
|
|
||||||
|
self.client.rbac = self.rbac
|
||||||
|
self.client.query = self.manager
|
||||||
|
|
||||||
|
def test_list(self):
|
||||||
|
returned_by_prom = {'data': ['metric1', 'test42', 'abc2']}
|
||||||
|
expected = ['abc2', 'metric1', 'test42']
|
||||||
|
|
||||||
|
with mock.patch.object(prometheus_client.PrometheusAPIClient, '_get',
|
||||||
|
return_value=returned_by_prom):
|
||||||
|
ret1 = self.manager.list()
|
||||||
|
ret2 = self.manager.list(disable_rbac=True)
|
||||||
|
|
||||||
|
self.assertEqual(expected, ret1)
|
||||||
|
self.assertEqual(expected, ret2)
|
||||||
|
|
||||||
|
def test_show(self):
|
||||||
|
query = 'some_metric'
|
||||||
|
returned_by_prom = {
|
||||||
|
'data': {
|
||||||
|
'resultType': 'non-vector'
|
||||||
|
},
|
||||||
|
'value': [1234567, 42],
|
||||||
|
'metric': {
|
||||||
|
'label': 'label_value'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
expected = [prometheus_client.PrometheusMetric(returned_by_prom)]
|
||||||
|
expected_matcher = MetricListMatcher(expected)
|
||||||
|
with mock.patch.object(prometheus_client.PrometheusAPIClient, '_get',
|
||||||
|
return_value=returned_by_prom):
|
||||||
|
ret1 = self.manager.show(query)
|
||||||
|
self.rbac.append_rbac.assert_called_with(query,
|
||||||
|
disable_rbac=False)
|
||||||
|
|
||||||
|
ret2 = self.manager.show(query, disable_rbac=True)
|
||||||
|
self.rbac.append_rbac.assert_called_with(query,
|
||||||
|
disable_rbac=True)
|
||||||
|
|
||||||
|
self.assertThat(ret1, expected_matcher)
|
||||||
|
self.assertThat(ret2, expected_matcher)
|
||||||
|
|
||||||
|
def test_query(self):
|
||||||
|
query = 'some_metric'
|
||||||
|
returned_by_prom = {
|
||||||
|
'data': {
|
||||||
|
'resultType': 'non-vector'
|
||||||
|
},
|
||||||
|
'value': [1234567, 42],
|
||||||
|
'metric': {
|
||||||
|
'label': 'label_value'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
expected = [prometheus_client.PrometheusMetric(returned_by_prom)]
|
||||||
|
expected_matcher = MetricListMatcher(expected)
|
||||||
|
with mock.patch.object(prometheus_client.PrometheusAPIClient, '_get',
|
||||||
|
return_value=returned_by_prom):
|
||||||
|
ret1 = self.manager.query(query)
|
||||||
|
self.rbac.enrich_query.assert_called_with(query,
|
||||||
|
disable_rbac=False)
|
||||||
|
|
||||||
|
ret2 = self.manager.query(query, disable_rbac=True)
|
||||||
|
self.rbac.enrich_query.assert_called_with(query,
|
||||||
|
disable_rbac=True)
|
||||||
|
|
||||||
|
self.assertThat(ret1, expected_matcher)
|
||||||
|
self.assertThat(ret2, expected_matcher)
|
||||||
|
|
||||||
|
def test_delete(self):
|
||||||
|
matches = "some_metric"
|
||||||
|
start = 0
|
||||||
|
end = 100
|
||||||
|
with mock.patch.object(prometheus_client.PrometheusAPIClient,
|
||||||
|
'delete') as m:
|
||||||
|
self.manager.delete(matches, start, end)
|
||||||
|
m.assert_called_with(matches, start, end)
|
||||||
|
|
||||||
|
def test_clean_tombstones(self):
|
||||||
|
with mock.patch.object(prometheus_client.PrometheusAPIClient,
|
||||||
|
'clean_tombstones') as m:
|
||||||
|
self.manager.clean_tombstones()
|
||||||
|
m.assert_called_once()
|
||||||
|
|
||||||
|
def test_snapshot(self):
|
||||||
|
with mock.patch.object(prometheus_client.PrometheusAPIClient,
|
||||||
|
'snapshot') as m:
|
||||||
|
self.manager.snapshot()
|
||||||
|
m.assert_called_once()
|
146
observabilityclient/tests/unit/test_rbac.py
Normal file
146
observabilityclient/tests/unit/test_rbac.py
Normal file
@ -0,0 +1,146 @@
|
|||||||
|
# Copyright 2023 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 keystoneauth1.exceptions.auth_plugins import MissingAuthPlugin
|
||||||
|
from keystoneauth1 import session
|
||||||
|
|
||||||
|
import testtools
|
||||||
|
|
||||||
|
from observabilityclient.v1 import rbac
|
||||||
|
|
||||||
|
|
||||||
|
class RbacTest(testtools.TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
super(RbacTest, self).setUp()
|
||||||
|
self.rbac = rbac.Rbac(mock.Mock(), mock.Mock())
|
||||||
|
self.rbac.project_id = "secret_id"
|
||||||
|
self.rbac.default_labels = {
|
||||||
|
"project": self.rbac.project_id
|
||||||
|
}
|
||||||
|
|
||||||
|
def test_constructor(self):
|
||||||
|
with mock.patch.object(session.Session, 'get_project_id',
|
||||||
|
return_value="123"):
|
||||||
|
r = rbac.Rbac("client", session.Session(), False)
|
||||||
|
self.assertEqual(r.project_id, "123")
|
||||||
|
self.assertEqual(r.default_labels, {
|
||||||
|
"project": "123"
|
||||||
|
})
|
||||||
|
|
||||||
|
def test_constructor_error(self):
|
||||||
|
with mock.patch.object(session.Session, 'get_project_id',
|
||||||
|
side_effect=MissingAuthPlugin()):
|
||||||
|
r = rbac.Rbac("client", session.Session(), False)
|
||||||
|
self.assertEqual(r.project_id, None)
|
||||||
|
|
||||||
|
def test_enrich_query(self):
|
||||||
|
test_cases = [
|
||||||
|
(
|
||||||
|
"test_query",
|
||||||
|
f"test_query{{project='{self.rbac.project_id}'}}"
|
||||||
|
), (
|
||||||
|
"test_query{somelabel='value'}",
|
||||||
|
|
||||||
|
(f"test_query{{somelabel='value', "
|
||||||
|
f"project='{self.rbac.project_id}'}}")
|
||||||
|
), (
|
||||||
|
"test_query{somelabel='value', label2='value2'}",
|
||||||
|
|
||||||
|
(f"test_query{{somelabel='value', label2='value2', "
|
||||||
|
f"project='{self.rbac.project_id}'}}")
|
||||||
|
), (
|
||||||
|
"test_query{somelabel='unicode{}{'}",
|
||||||
|
|
||||||
|
(f"test_query{{somelabel='unicode{{}}{{', "
|
||||||
|
f"project='{self.rbac.project_id}'}}")
|
||||||
|
), (
|
||||||
|
"test_query{doesnt_match_regex!~'regex'}",
|
||||||
|
|
||||||
|
(f"test_query{{doesnt_match_regex!~'regex', "
|
||||||
|
f"project='{self.rbac.project_id}'}}")
|
||||||
|
), (
|
||||||
|
"delta(cpu_temp_celsius{host='zeus'}[2h]) - "
|
||||||
|
"sum(http_requests) + "
|
||||||
|
"sum(http_requests{instance=~'.*'}) + "
|
||||||
|
"sum(http_requests{or_regex=~'smth1|something2|3'})",
|
||||||
|
|
||||||
|
(f"delta(cpu_temp_celsius{{host='zeus', "
|
||||||
|
f"project='{self.rbac.project_id}'}}[2h]) - "
|
||||||
|
f"sum(http_requests"
|
||||||
|
f"{{project='{self.rbac.project_id}'}}) + "
|
||||||
|
f"sum(http_requests{{instance=~'.*', "
|
||||||
|
f"project='{self.rbac.project_id}'}}) + "
|
||||||
|
f"sum(http_requests{{or_regex=~'smth1|something2|3', "
|
||||||
|
f"project='{self.rbac.project_id}'}})")
|
||||||
|
)
|
||||||
|
]
|
||||||
|
|
||||||
|
self.rbac.client.query.list = lambda disable_rbac: ['test_query',
|
||||||
|
'cpu_temp_celsius',
|
||||||
|
'http_requests']
|
||||||
|
|
||||||
|
for query, expected in test_cases:
|
||||||
|
ret = self.rbac.enrich_query(query)
|
||||||
|
self.assertEqual(ret, expected)
|
||||||
|
|
||||||
|
def test_enrich_query_disable(self):
|
||||||
|
test_cases = [
|
||||||
|
(
|
||||||
|
"test_query",
|
||||||
|
"test_query"
|
||||||
|
), (
|
||||||
|
"test_query{somelabel='value'}",
|
||||||
|
"test_query{somelabel='value'}"
|
||||||
|
), (
|
||||||
|
"test_query{somelabel='value', label2='value2'}",
|
||||||
|
"test_query{somelabel='value', label2='value2'}"
|
||||||
|
), (
|
||||||
|
"test_query{somelabel='unicode{}{'}",
|
||||||
|
"test_query{somelabel='unicode{}{'}"
|
||||||
|
), (
|
||||||
|
"test_query{doesnt_match_regex!~'regex'}",
|
||||||
|
"test_query{doesnt_match_regex!~'regex'}",
|
||||||
|
), (
|
||||||
|
"delta(cpu_temp_celsius{host='zeus'}[2h]) - "
|
||||||
|
"sum(http_requests) + "
|
||||||
|
"sum(http_requests{instance=~'.*'}) + "
|
||||||
|
"sum(http_requests{or_regex=~'smth1|something2|3'})",
|
||||||
|
|
||||||
|
"delta(cpu_temp_celsius{host='zeus'}[2h]) - "
|
||||||
|
"sum(http_requests) + "
|
||||||
|
"sum(http_requests{instance=~'.*'}) + "
|
||||||
|
"sum(http_requests{or_regex=~'smth1|something2|3'})"
|
||||||
|
)
|
||||||
|
]
|
||||||
|
|
||||||
|
self.rbac.client.query.list = lambda disable_rbac: ['test_query',
|
||||||
|
'cpu_temp_celsius',
|
||||||
|
'http_requests']
|
||||||
|
for query, expected in test_cases:
|
||||||
|
ret = self.rbac.enrich_query(query, disable_rbac=True)
|
||||||
|
self.assertEqual(ret, query)
|
||||||
|
|
||||||
|
def test_append_rbac(self):
|
||||||
|
query = "test_query"
|
||||||
|
expected = f"{query}{{project='{self.rbac.project_id}'}}"
|
||||||
|
ret = self.rbac.append_rbac(query)
|
||||||
|
self.assertEqual(ret, expected)
|
||||||
|
|
||||||
|
def test_append_rbac_disable(self):
|
||||||
|
query = "test_query"
|
||||||
|
expected = query
|
||||||
|
ret = self.rbac.append_rbac(query, disable_rbac=True)
|
||||||
|
self.assertEqual(ret, expected)
|
126
observabilityclient/tests/unit/test_utils.py
Normal file
126
observabilityclient/tests/unit/test_utils.py
Normal file
@ -0,0 +1,126 @@
|
|||||||
|
# Copyright 2023 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.
|
||||||
|
|
||||||
|
import os
|
||||||
|
from unittest import mock
|
||||||
|
|
||||||
|
import testtools
|
||||||
|
|
||||||
|
from observabilityclient import prometheus_client
|
||||||
|
from observabilityclient.utils import metric_utils
|
||||||
|
|
||||||
|
|
||||||
|
class GetConfigFileTest(testtools.TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
super(GetConfigFileTest, self).setUp()
|
||||||
|
|
||||||
|
def test_current_dir(self):
|
||||||
|
with (mock.patch.object(os.path, 'exists', return_value=True),
|
||||||
|
mock.patch.object(metric_utils, 'open') as m):
|
||||||
|
metric_utils.get_config_file()
|
||||||
|
m.assert_called_with(metric_utils.CONFIG_FILE_NAME, 'r')
|
||||||
|
|
||||||
|
def test_path_order(self):
|
||||||
|
expected = [mock.call(metric_utils.CONFIG_FILE_NAME, 'r'),
|
||||||
|
mock.call((f"{os.environ['HOME']}/.config/openstack/"
|
||||||
|
f"{metric_utils.CONFIG_FILE_NAME}")),
|
||||||
|
mock.call((f"/etc/openstack/"
|
||||||
|
f"{metric_utils.CONFIG_FILE_NAME}"))]
|
||||||
|
with mock.patch.object(os.path, 'exists', return_value=False) as m:
|
||||||
|
ret = metric_utils.get_config_file()
|
||||||
|
m.call_args_list == expected
|
||||||
|
self.assertEqual(ret, None)
|
||||||
|
|
||||||
|
|
||||||
|
class GetPrometheusClientTest(testtools.TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
super(GetPrometheusClientTest, self).setUp()
|
||||||
|
config_data = 'host: "somehost"\nport: "1234"'
|
||||||
|
self.config_file = mock.mock_open(read_data=config_data)("name", 'r')
|
||||||
|
|
||||||
|
def test_get_prometheus_client_from_file(self):
|
||||||
|
with (mock.patch.object(metric_utils, 'get_config_file',
|
||||||
|
return_value=self.config_file),
|
||||||
|
mock.patch.object(prometheus_client.PrometheusAPIClient,
|
||||||
|
"__init__", return_value=None) as m):
|
||||||
|
metric_utils.get_prometheus_client()
|
||||||
|
m.assert_called_with("somehost:1234")
|
||||||
|
|
||||||
|
def test_get_prometheus_client_env_overide(self):
|
||||||
|
with (mock.patch.dict(os.environ, {'PROMETHEUS_HOST': 'env_overide'}),
|
||||||
|
mock.patch.object(metric_utils, 'get_config_file',
|
||||||
|
return_value=self.config_file),
|
||||||
|
mock.patch.object(prometheus_client.PrometheusAPIClient,
|
||||||
|
"__init__", return_value=None) as m):
|
||||||
|
metric_utils.get_prometheus_client()
|
||||||
|
m.assert_called_with("env_overide:1234")
|
||||||
|
|
||||||
|
def test_get_prometheus_client_no_config_file(self):
|
||||||
|
patched_env = {'PROMETHEUS_HOST': 'env_overide',
|
||||||
|
'PROMETHEUS_PORT': 'env_port'}
|
||||||
|
with (mock.patch.dict(os.environ, patched_env),
|
||||||
|
mock.patch.object(prometheus_client.PrometheusAPIClient,
|
||||||
|
"__init__", return_value=None) as m):
|
||||||
|
metric_utils.get_prometheus_client()
|
||||||
|
m.assert_called_with("env_overide:env_port")
|
||||||
|
|
||||||
|
def test_get_prometheus_client_missing_configuration(self):
|
||||||
|
with (mock.patch.dict(os.environ, {}),
|
||||||
|
mock.patch.object(prometheus_client.PrometheusAPIClient,
|
||||||
|
"__init__", return_value=None)):
|
||||||
|
self.assertRaises(metric_utils.ConfigurationError,
|
||||||
|
metric_utils.get_prometheus_client)
|
||||||
|
|
||||||
|
|
||||||
|
class FormatLabelsTest(testtools.TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
super(FormatLabelsTest, self).setUp()
|
||||||
|
|
||||||
|
def test_format_labels_with_normal_labels(self):
|
||||||
|
input_dict = {"label_key1": "label_value1",
|
||||||
|
"label_key2": "label_value2"}
|
||||||
|
expected = "label_key1='label_value1', label_key2='label_value2'"
|
||||||
|
|
||||||
|
ret = metric_utils.format_labels(input_dict)
|
||||||
|
self.assertEqual(expected, ret)
|
||||||
|
|
||||||
|
def test_format_labels_with_quoted_labels(self):
|
||||||
|
input_dict = {"label_key1": "'label_value1'",
|
||||||
|
"label_key2": "'label_value2'"}
|
||||||
|
expected = "label_key1='label_value1', label_key2='label_value2'"
|
||||||
|
|
||||||
|
ret = metric_utils.format_labels(input_dict)
|
||||||
|
self.assertEqual(expected, ret)
|
||||||
|
|
||||||
|
|
||||||
|
class Metrics2ColsTest(testtools.TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
super(Metrics2ColsTest, self).setUp()
|
||||||
|
|
||||||
|
def test_metrics2cols(self):
|
||||||
|
metric = {
|
||||||
|
'value': [
|
||||||
|
1234567,
|
||||||
|
5
|
||||||
|
],
|
||||||
|
'metric': {
|
||||||
|
'label1': 'value1',
|
||||||
|
'label2': 'value2',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
input_metrics = [prometheus_client.PrometheusMetric(metric)]
|
||||||
|
expected = (['label1', 'label2', 'value'], [['value1', 'value2', 5]])
|
||||||
|
|
||||||
|
ret = metric_utils.metrics2cols(input_metrics)
|
||||||
|
self.assertEqual(expected, ret)
|
@ -14,10 +14,12 @@
|
|||||||
|
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
|
|
||||||
import yaml
|
import yaml
|
||||||
|
|
||||||
from observabilityclient.prometheus_client import PrometheusAPIClient
|
from observabilityclient.prometheus_client import PrometheusAPIClient
|
||||||
|
|
||||||
|
|
||||||
DEFAULT_CONFIG_LOCATIONS = [os.environ["HOME"] + "/.config/openstack/",
|
DEFAULT_CONFIG_LOCATIONS = [os.environ["HOME"] + "/.config/openstack/",
|
||||||
"/etc/openstack/"]
|
"/etc/openstack/"]
|
||||||
CONFIG_FILE_NAME = "prometheus.yaml"
|
CONFIG_FILE_NAME = "prometheus.yaml"
|
||||||
@ -30,12 +32,12 @@ class ConfigurationError(Exception):
|
|||||||
|
|
||||||
def get_config_file():
|
def get_config_file():
|
||||||
if os.path.exists(CONFIG_FILE_NAME):
|
if os.path.exists(CONFIG_FILE_NAME):
|
||||||
LOG.debug(f"Using {CONFIG_FILE_NAME} as prometheus configuration")
|
LOG.debug("Using %s as prometheus configuration", CONFIG_FILE_NAME)
|
||||||
return open(CONFIG_FILE_NAME, "r")
|
return open(CONFIG_FILE_NAME, "r")
|
||||||
for path in DEFAULT_CONFIG_LOCATIONS:
|
for path in DEFAULT_CONFIG_LOCATIONS:
|
||||||
full_filename = path + CONFIG_FILE_NAME
|
full_filename = path + CONFIG_FILE_NAME
|
||||||
if os.path.exists(full_filename):
|
if os.path.exists(full_filename):
|
||||||
LOG.debug(f"Using {full_filename} as prometheus configuration")
|
LOG.debug("Using %s as prometheus configuration", full_filename)
|
||||||
return open(full_filename, "r")
|
return open(full_filename, "r")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@ -68,11 +70,6 @@ def get_client(obj):
|
|||||||
return obj.app.client_manager.observabilityclient
|
return obj.app.client_manager.observabilityclient
|
||||||
|
|
||||||
|
|
||||||
def list2cols(cols, objs):
|
|
||||||
return cols, [tuple([o[k] for k in cols])
|
|
||||||
for o in objs]
|
|
||||||
|
|
||||||
|
|
||||||
def format_labels(d: dict) -> str:
|
def format_labels(d: dict) -> str:
|
||||||
def replace_doubled_quotes(string):
|
def replace_doubled_quotes(string):
|
||||||
if "''" in string:
|
if "''" in string:
|
||||||
|
@ -44,6 +44,7 @@ class ObservabilityBaseCommand(command.Command):
|
|||||||
|
|
||||||
class Manager(object):
|
class Manager(object):
|
||||||
"""Base class for the python api."""
|
"""Base class for the python api."""
|
||||||
|
|
||||||
DEFAULT_HEADERS = {
|
DEFAULT_HEADERS = {
|
||||||
"Accept": "application/json",
|
"Accept": "application/json",
|
||||||
}
|
}
|
||||||
|
@ -12,24 +12,25 @@
|
|||||||
# License for the specific language governing permissions and limitations
|
# License for the specific language governing permissions and limitations
|
||||||
# under the License.
|
# under the License.
|
||||||
|
|
||||||
from observabilityclient.utils import metric_utils
|
from cliff import lister
|
||||||
from observabilityclient.v1 import base
|
|
||||||
from osc_lib.i18n import _
|
from osc_lib.i18n import _
|
||||||
|
|
||||||
from cliff import lister
|
from observabilityclient.utils import metric_utils
|
||||||
|
from observabilityclient.v1 import base
|
||||||
|
|
||||||
|
|
||||||
class List(base.ObservabilityBaseCommand, lister.Lister):
|
class List(base.ObservabilityBaseCommand, lister.Lister):
|
||||||
"""Query prometheus for list of all metrics"""
|
"""Query prometheus for list of all metrics."""
|
||||||
|
|
||||||
def take_action(self, parsed_args):
|
def take_action(self, parsed_args):
|
||||||
client = metric_utils.get_client(self)
|
client = metric_utils.get_client(self)
|
||||||
metrics = client.query.list(disable_rbac=parsed_args.disable_rbac)
|
metrics = client.query.list(disable_rbac=parsed_args['disable_rbac'])
|
||||||
return ["metric_name"], [[m] for m in metrics]
|
return ["metric_name"], [[m] for m in metrics]
|
||||||
|
|
||||||
|
|
||||||
class Show(base.ObservabilityBaseCommand, lister.Lister):
|
class Show(base.ObservabilityBaseCommand, lister.Lister):
|
||||||
"""Query prometheus for the current value of metric"""
|
"""Query prometheus for the current value of metric."""
|
||||||
|
|
||||||
def get_parser(self, prog_name):
|
def get_parser(self, prog_name):
|
||||||
parser = super().get_parser(prog_name)
|
parser = super().get_parser(prog_name)
|
||||||
@ -40,13 +41,14 @@ class Show(base.ObservabilityBaseCommand, lister.Lister):
|
|||||||
|
|
||||||
def take_action(self, parsed_args):
|
def take_action(self, parsed_args):
|
||||||
client = metric_utils.get_client(self)
|
client = metric_utils.get_client(self)
|
||||||
metric = client.query.show(parsed_args.name,
|
metric = client.query.show(parsed_args['name'],
|
||||||
disable_rbac=parsed_args.disable_rbac)
|
disable_rbac=parsed_args['disable_rbac'])
|
||||||
return metric_utils.metrics2cols(metric)
|
ret = metric_utils.metrics2cols(metric)
|
||||||
|
return ret
|
||||||
|
|
||||||
|
|
||||||
class Query(base.ObservabilityBaseCommand, lister.Lister):
|
class Query(base.ObservabilityBaseCommand, lister.Lister):
|
||||||
"""Query prometheus with a custom query string"""
|
"""Query prometheus with a custom query string."""
|
||||||
|
|
||||||
def get_parser(self, prog_name):
|
def get_parser(self, prog_name):
|
||||||
parser = super().get_parser(prog_name)
|
parser = super().get_parser(prog_name)
|
||||||
@ -57,14 +59,15 @@ class Query(base.ObservabilityBaseCommand, lister.Lister):
|
|||||||
|
|
||||||
def take_action(self, parsed_args):
|
def take_action(self, parsed_args):
|
||||||
client = metric_utils.get_client(self)
|
client = metric_utils.get_client(self)
|
||||||
metric = client.query.query(parsed_args.query,
|
metric = client.query.query(parsed_args['query'],
|
||||||
disable_rbac=parsed_args.disable_rbac)
|
disable_rbac=parsed_args['disable_rbac'])
|
||||||
ret = metric_utils.metrics2cols(metric)
|
ret = metric_utils.metrics2cols(metric)
|
||||||
return ret
|
return ret
|
||||||
|
|
||||||
|
|
||||||
class Delete(base.ObservabilityBaseCommand):
|
class Delete(base.ObservabilityBaseCommand):
|
||||||
"""Delete data for a selected series and time range"""
|
"""Delete data for a selected series and time range."""
|
||||||
|
|
||||||
def get_parser(self, prog_name):
|
def get_parser(self, prog_name):
|
||||||
parser = super().get_parser(prog_name)
|
parser = super().get_parser(prog_name)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
@ -86,13 +89,14 @@ class Delete(base.ObservabilityBaseCommand):
|
|||||||
|
|
||||||
def take_action(self, parsed_args):
|
def take_action(self, parsed_args):
|
||||||
client = metric_utils.get_client(self)
|
client = metric_utils.get_client(self)
|
||||||
return client.query.delete(parsed_args.matches,
|
return client.query.delete(parsed_args['matches'],
|
||||||
parsed_args.start,
|
parsed_args['start'],
|
||||||
parsed_args.end)
|
parsed_args['end'])
|
||||||
|
|
||||||
|
|
||||||
class CleanTombstones(base.ObservabilityBaseCommand):
|
class CleanTombstones(base.ObservabilityBaseCommand):
|
||||||
"""Remove deleted data from disk and clean up the existing tombstones"""
|
"""Remove deleted data from disk and clean up the existing tombstones."""
|
||||||
|
|
||||||
def get_parser(self, prog_name):
|
def get_parser(self, prog_name):
|
||||||
parser = super().get_parser(prog_name)
|
parser = super().get_parser(prog_name)
|
||||||
return parser
|
return parser
|
||||||
|
@ -20,7 +20,7 @@ from observabilityclient.v1 import rbac
|
|||||||
|
|
||||||
|
|
||||||
class Client(object):
|
class Client(object):
|
||||||
"""Client for the observabilityclient api"""
|
"""Client for the observabilityclient api."""
|
||||||
|
|
||||||
def __init__(self, session=None, adapter_options=None,
|
def __init__(self, session=None, adapter_options=None,
|
||||||
session_options=None, disable_rbac=False):
|
session_options=None, disable_rbac=False):
|
||||||
|
@ -18,14 +18,14 @@ from observabilityclient.v1 import base
|
|||||||
|
|
||||||
class QueryManager(base.Manager):
|
class QueryManager(base.Manager):
|
||||||
def list(self, disable_rbac=False):
|
def list(self, disable_rbac=False):
|
||||||
"""Lists metric names
|
"""List metric names.
|
||||||
|
|
||||||
:param disable_rbac: Disables rbac injection if set to True
|
:param disable_rbac: Disables rbac injection if set to True
|
||||||
:type disable_rbac: boolean
|
:type disable_rbac: boolean
|
||||||
"""
|
"""
|
||||||
if disable_rbac or self.client.rbac.disable_rbac:
|
if disable_rbac or self.client.rbac.disable_rbac:
|
||||||
metric_names = self.prom.label_values("__name__")
|
metric_names = self.prom.label_values("__name__")
|
||||||
return metric_names
|
return sorted(metric_names)
|
||||||
else:
|
else:
|
||||||
match = f"{{{format_labels(self.client.rbac.default_labels)}}}"
|
match = f"{{{format_labels(self.client.rbac.default_labels)}}}"
|
||||||
metrics = self.prom.series(match)
|
metrics = self.prom.series(match)
|
||||||
@ -35,7 +35,7 @@ class QueryManager(base.Manager):
|
|||||||
return sorted(unique_metric_names)
|
return sorted(unique_metric_names)
|
||||||
|
|
||||||
def show(self, name, disable_rbac=False):
|
def show(self, name, disable_rbac=False):
|
||||||
"""Shows current values for metrics of a specified name
|
"""Show current values for metrics of a specified name.
|
||||||
|
|
||||||
:param disable_rbac: Disables rbac injection if set to True
|
:param disable_rbac: Disables rbac injection if set to True
|
||||||
:type disable_rbac: boolean
|
:type disable_rbac: boolean
|
||||||
@ -46,7 +46,7 @@ class QueryManager(base.Manager):
|
|||||||
return self.prom.query(last_metric_query)
|
return self.prom.query(last_metric_query)
|
||||||
|
|
||||||
def query(self, query, disable_rbac=False):
|
def query(self, query, disable_rbac=False):
|
||||||
"""Sends a query to prometheus
|
"""Send a query to prometheus.
|
||||||
|
|
||||||
The query can be any PromQL query. Labels for enforcing
|
The query can be any PromQL query. Labels for enforcing
|
||||||
rbac will be added to all of the metric name inside the query.
|
rbac will be added to all of the metric name inside the query.
|
||||||
@ -63,11 +63,11 @@ class QueryManager(base.Manager):
|
|||||||
:param disable_rbac: Disables rbac injection if set to True
|
:param disable_rbac: Disables rbac injection if set to True
|
||||||
:type disable_rbac: boolean
|
:type disable_rbac: boolean
|
||||||
"""
|
"""
|
||||||
query = self.client.rbac.enrich_query(query, disable_rbac)
|
query = self.client.rbac.enrich_query(query, disable_rbac=disable_rbac)
|
||||||
return self.prom.query(query)
|
return self.prom.query(query)
|
||||||
|
|
||||||
def delete(self, matches, start=None, end=None):
|
def delete(self, matches, start=None, end=None):
|
||||||
"""Deletes metrics from Prometheus
|
"""Delete metrics from Prometheus.
|
||||||
|
|
||||||
The metrics aren't deleted immediately. Do a call to clean_tombstones()
|
The metrics aren't deleted immediately. Do a call to clean_tombstones()
|
||||||
to speed up the deletion. If start and end isn't specified, then
|
to speed up the deletion. If start and end isn't specified, then
|
||||||
@ -88,9 +88,9 @@ class QueryManager(base.Manager):
|
|||||||
return self.prom.delete(matches, start, end)
|
return self.prom.delete(matches, start, end)
|
||||||
|
|
||||||
def clean_tombstones(self):
|
def clean_tombstones(self):
|
||||||
"""Instructs prometheus to clean tombstones"""
|
"""Instruct prometheus to clean tombstones."""
|
||||||
return self.prom.clean_tombstones()
|
return self.prom.clean_tombstones()
|
||||||
|
|
||||||
def snapshot(self):
|
def snapshot(self):
|
||||||
"Creates a snapshot of the current data"
|
"""Create a snapshot of the current data."""
|
||||||
return self.prom.snapshot()
|
return self.prom.snapshot()
|
||||||
|
@ -12,10 +12,12 @@
|
|||||||
# License for the specific language governing permissions and limitations
|
# License for the specific language governing permissions and limitations
|
||||||
# under the License.
|
# under the License.
|
||||||
|
|
||||||
from keystoneauth1.exceptions.auth_plugins import MissingAuthPlugin
|
|
||||||
from observabilityclient.utils.metric_utils import format_labels
|
|
||||||
import re
|
import re
|
||||||
|
|
||||||
|
from keystoneauth1.exceptions.auth_plugins import MissingAuthPlugin
|
||||||
|
|
||||||
|
from observabilityclient.utils.metric_utils import format_labels
|
||||||
|
|
||||||
|
|
||||||
class ObservabilityRbacError(Exception):
|
class ObservabilityRbacError(Exception):
|
||||||
pass
|
pass
|
||||||
@ -48,13 +50,22 @@ class Rbac():
|
|||||||
# returns the quote position or -1
|
# returns the quote position or -1
|
||||||
return end
|
return end
|
||||||
|
|
||||||
def _find_label_pair_end(self, query, start):
|
def _find_match_operator(self, query, start):
|
||||||
eq_sign_pos = query.find('=', start)
|
eq_sign_pos = query.find('=', start)
|
||||||
|
tilde_pos = query.find('~', start)
|
||||||
|
if eq_sign_pos == -1:
|
||||||
|
return tilde_pos
|
||||||
|
if tilde_pos == -1:
|
||||||
|
return eq_sign_pos
|
||||||
|
return min(eq_sign_pos, tilde_pos)
|
||||||
|
|
||||||
|
def _find_label_pair_end(self, query, start):
|
||||||
|
match_operator_pos = self._find_match_operator(query, start)
|
||||||
quote_char = "'"
|
quote_char = "'"
|
||||||
quote_start_pos = query.find(quote_char, eq_sign_pos)
|
quote_start_pos = query.find(quote_char, match_operator_pos)
|
||||||
if quote_start_pos == -1:
|
if quote_start_pos == -1:
|
||||||
quote_char = '"'
|
quote_char = '"'
|
||||||
quote_start_pos = query.find(quote_char, eq_sign_pos)
|
quote_start_pos = query.find(quote_char, match_operator_pos)
|
||||||
end = self._find_label_value_end(query, quote_start_pos, quote_char)
|
end = self._find_label_value_end(query, quote_start_pos, quote_char)
|
||||||
# returns the pair end or -1
|
# returns the pair end or -1
|
||||||
return end
|
return end
|
||||||
@ -64,18 +75,19 @@ class Rbac():
|
|||||||
while nearest_curly_brace_pos != -1:
|
while nearest_curly_brace_pos != -1:
|
||||||
pair_end = self._find_label_pair_end(query, start)
|
pair_end = self._find_label_pair_end(query, start)
|
||||||
nearest_curly_brace_pos = query.find("}", pair_end)
|
nearest_curly_brace_pos = query.find("}", pair_end)
|
||||||
nearest_eq_sign_pos = query.find("=", pair_end)
|
nearest_match_operator_pos = self._find_match_operator(query,
|
||||||
if (nearest_curly_brace_pos < nearest_eq_sign_pos or
|
pair_end)
|
||||||
nearest_eq_sign_pos == -1):
|
if (nearest_curly_brace_pos < nearest_match_operator_pos or
|
||||||
# If we have "}" before the nearest "=",
|
nearest_match_operator_pos == -1):
|
||||||
|
# If we have "}" before the nearest "=" or "~",
|
||||||
# then we must be at the end of the label section
|
# then we must be at the end of the label section
|
||||||
# and the "=" is a part of the next section.
|
# and the "=" or "~" is a part of the next section.
|
||||||
return nearest_curly_brace_pos
|
return nearest_curly_brace_pos
|
||||||
start = pair_end
|
start = pair_end
|
||||||
return -1
|
return -1
|
||||||
|
|
||||||
def enrich_query(self, query, disable_rbac=False):
|
def enrich_query(self, query, disable_rbac=False):
|
||||||
"""Used to add rbac labels to queries
|
"""Add rbac labels to queries.
|
||||||
|
|
||||||
:param query: The query to enrich
|
:param query: The query to enrich
|
||||||
:type query: str
|
:type query: str
|
||||||
@ -121,7 +133,7 @@ class Rbac():
|
|||||||
return query
|
return query
|
||||||
|
|
||||||
def append_rbac(self, query, disable_rbac=False):
|
def append_rbac(self, query, disable_rbac=False):
|
||||||
"""Used to append rbac labels to queries
|
"""Append rbac labels to queries.
|
||||||
|
|
||||||
It's a simplified and faster version of enrich_query(). This just
|
It's a simplified and faster version of enrich_query(). This just
|
||||||
appends the labels at the end of the query string. For proper handling
|
appends the labels at the end of the query string. For proper handling
|
||||||
|
3
requirements.txt
Normal file
3
requirements.txt
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
osc-lib>=1.0.1 # Apache-2.0
|
||||||
|
keystoneauth1>=1.0.0
|
||||||
|
cliff!=1.16.0,>=1.14.0 # Apache-2.0
|
12
setup.cfg
12
setup.cfg
@ -34,6 +34,18 @@ classifier =
|
|||||||
packages =
|
packages =
|
||||||
observabilityclient
|
observabilityclient
|
||||||
|
|
||||||
|
[options.extras_require]
|
||||||
|
test =
|
||||||
|
coverage>=3.6
|
||||||
|
oslotest>=1.10.0 # Apache-2.0
|
||||||
|
reno>=1.6.2 # Apache2
|
||||||
|
tempest>=10
|
||||||
|
stestr>=2.0.0 # Apache-2.0
|
||||||
|
testtools>=1.4.0
|
||||||
|
pifpaf[gnocchi]>=0.23
|
||||||
|
SQLAlchemy<=1.4.41
|
||||||
|
oslo.db<=12.3.1
|
||||||
|
|
||||||
[entry_points]
|
[entry_points]
|
||||||
openstack.cli.extension =
|
openstack.cli.extension =
|
||||||
observabilityclient = observabilityclient.plugin
|
observabilityclient = observabilityclient.plugin
|
||||||
|
25
tools/install_deps.sh
Executable file
25
tools/install_deps.sh
Executable file
@ -0,0 +1,25 @@
|
|||||||
|
#!/bin/bash -ex
|
||||||
|
|
||||||
|
sudo apt-get update -y && sudo apt-get install -qy gnupg software-properties-common
|
||||||
|
sudo add-apt-repository -y ppa:deadsnakes/ppa
|
||||||
|
sudo apt-get update -y && sudo apt-get install -qy \
|
||||||
|
locales \
|
||||||
|
git \
|
||||||
|
wget \
|
||||||
|
curl \
|
||||||
|
python3 \
|
||||||
|
python3-dev \
|
||||||
|
python3-pip \
|
||||||
|
python3.9 \
|
||||||
|
python3.9-dev \
|
||||||
|
python3.9-distutils \
|
||||||
|
python3.11 \
|
||||||
|
python3.11-dev
|
||||||
|
|
||||||
|
sudo rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
export LANG=en_US.UTF-8
|
||||||
|
sudo update-locale
|
||||||
|
sudo locale-gen $LANG
|
||||||
|
|
||||||
|
sudo python3 -m pip install -U pip tox virtualenv
|
47
tox.ini
Normal file
47
tox.ini
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
[tox]
|
||||||
|
minversion = 4.2.5
|
||||||
|
envlist = py38,py39,py311,pep8
|
||||||
|
ignore_basepython_conflict = True
|
||||||
|
|
||||||
|
[testenv]
|
||||||
|
basepython = python3
|
||||||
|
usedevelop = True
|
||||||
|
setenv =
|
||||||
|
VIRTUAL_ENV={envdir}
|
||||||
|
OBSERVABILITY_CLIENT_EXEC_DIR={envdir}/bin
|
||||||
|
passenv =
|
||||||
|
PROMETHEUS_*
|
||||||
|
OBSERVABILITY_*
|
||||||
|
deps = .[test]
|
||||||
|
pytest
|
||||||
|
commands = pytest {posargs:observabilityclient/tests}
|
||||||
|
|
||||||
|
[testenv:pep8]
|
||||||
|
basepython = python3
|
||||||
|
deps = flake8
|
||||||
|
flake8-blind-except
|
||||||
|
flake8-builtins
|
||||||
|
flake8-docstrings
|
||||||
|
flake8-logging-format
|
||||||
|
hacking<3.1.0,>=3.0
|
||||||
|
commands = flake8
|
||||||
|
|
||||||
|
[testenv:venv]
|
||||||
|
deps = .[test,doc]
|
||||||
|
commands = {posargs}
|
||||||
|
|
||||||
|
[testenv:cover]
|
||||||
|
deps = {[testenv]deps}
|
||||||
|
pytest-cov
|
||||||
|
commands = observabilityclient {posargs:observabilityclient/tests}
|
||||||
|
|
||||||
|
[flake8]
|
||||||
|
show-source = True
|
||||||
|
ignore = D100,D101,D102,D103,D104,D105,D106,D107,A002,A003,W504,W503
|
||||||
|
exclude=.git,.tox,dist,doc,*egg,build
|
||||||
|
enable-extensions=G
|
||||||
|
application-import-names = observabilityclient
|
||||||
|
|
||||||
|
[pytest]
|
||||||
|
addopts = --verbose
|
||||||
|
norecursedirs = .tox
|
Loading…
Reference in New Issue
Block a user