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:
		
							
								
								
									
										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,87 +12,91 @@ | |||||||
| #   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) | ||||||
|         parser.add_argument( |         parser.add_argument( | ||||||
|                 'name', |             'name', | ||||||
|                 help=_("Name of the metric to show")) |             help=_("Name of the metric to show")) | ||||||
|         return parser |         return parser | ||||||
|  |  | ||||||
|     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) | ||||||
|         parser.add_argument( |         parser.add_argument( | ||||||
|                 'query', |             'query', | ||||||
|                 help=_("Custom PromQL query")) |             help=_("Custom PromQL query")) | ||||||
|         return parser |         return parser | ||||||
|  |  | ||||||
|     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( | ||||||
|                 'matches', |             'matches', | ||||||
|                 action="append", |             action="append", | ||||||
|                 nargs='+', |             nargs='+', | ||||||
|                 help=_("Series selector, that selects the series to delete. " |             help=_("Series selector, that selects the series to delete. " | ||||||
|                        "Specify multiple selectors delimited by space to " |                    "Specify multiple selectors delimited by space to " | ||||||
|                        "delete multiple series.")) |                    "delete multiple series.")) | ||||||
|         parser.add_argument( |         parser.add_argument( | ||||||
|                 '--start', |             '--start', | ||||||
|                 help=_("Start timestamp in rfc3339 or unix timestamp. " |             help=_("Start timestamp in rfc3339 or unix timestamp. " | ||||||
|                        "Defaults to minimum possible timestamp.")) |                    "Defaults to minimum possible timestamp.")) | ||||||
|         parser.add_argument( |         parser.add_argument( | ||||||
|                 '--end', |             '--end', | ||||||
|                 help=_("End timestamp in rfc3339 or unix timestamp. " |             help=_("End timestamp in rfc3339 or unix timestamp. " | ||||||
|                        "Defaults to maximum possible timestamp.")) |                    "Defaults to maximum possible timestamp.")) | ||||||
|         return parser |         return parser | ||||||
|  |  | ||||||
|     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. | ||||||
| @@ -56,18 +56,18 @@ class QueryManager(base.Manager): | |||||||
|         query("sum(name1) - sum(name2{label1='value'})") |         query("sum(name1) - sum(name2{label1='value'})") | ||||||
|         will result in a query string like this: |         will result in a query string like this: | ||||||
|         "sum(name1{rbac='rbac_value'}) - |         "sum(name1{rbac='rbac_value'}) - | ||||||
|             sum(name2{label1='value', rbac='rbac_value'})" |         sum(name2{label1='value', rbac='rbac_value'})" | ||||||
|  |  | ||||||
|         :param query: Custom query string |         :param query: Custom query string | ||||||
|         :type query: str |         :type query: str | ||||||
|         :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 | ||||||
| @@ -29,14 +31,14 @@ class Rbac(): | |||||||
|         try: |         try: | ||||||
|             self.project_id = self.session.get_project_id() |             self.project_id = self.session.get_project_id() | ||||||
|             self.default_labels = { |             self.default_labels = { | ||||||
|                     "project": self.project_id |                 "project": self.project_id | ||||||
|                     } |             } | ||||||
|             self.rbac_init_successful = True |             self.rbac_init_successful = True | ||||||
|         except MissingAuthPlugin: |         except MissingAuthPlugin: | ||||||
|             self.project_id = None |             self.project_id = None | ||||||
|             self.default_labels = { |             self.default_labels = { | ||||||
|                     "project": "no-project" |                 "project": "no-project" | ||||||
|                     } |             } | ||||||
|             self.rbac_init_successful = False |             self.rbac_init_successful = False | ||||||
|  |  | ||||||
|     def _find_label_value_end(self, query, start, quote_char): |     def _find_label_value_end(self, query, start, quote_char): | ||||||
| @@ -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 | ||||||
		Reference in New Issue
	
	Block a user
	 Jaromír Wysoglad
					Jaromír Wysoglad