diff --git a/csp-billing-adapter-local.spec b/csp-billing-adapter-local.spec index 1814bd1..bdaf7b7 100644 --- a/csp-billing-adapter-local.spec +++ b/csp-billing-adapter-local.spec @@ -38,6 +38,7 @@ BuildRequires: %{python_module pytest-cov} %endif Requires: python-setuptools Requires: python-pluggy +Requires: python-requests Requires: python-csp-billing-adapter BuildArch: noarch %python_subpackages diff --git a/csp_billing_adapter_local/plugin.py b/csp_billing_adapter_local/plugin.py index af65386..57df0f8 100644 --- a/csp_billing_adapter_local/plugin.py +++ b/csp_billing_adapter_local/plugin.py @@ -20,6 +20,7 @@ import json import logging +import requests from json.decoder import JSONDecodeError from pathlib import Path @@ -27,6 +28,7 @@ import csp_billing_adapter from csp_billing_adapter.config import Config +from csp_billing_adapter.exceptions import CSPBillingAdapterException log = logging.getLogger('CSPBillingAdapter') @@ -111,3 +113,52 @@ def save_csp_config( ): """Save specified content as local storage csp_config contents.""" update_csp_config(config, csp_config, replace=True) + + +@csp_billing_adapter.hookimpl(trylast=True) +def get_usage_data(config: Config): + """ + Retrieves the current usage report from API + + :param config: The application configuration dictionary + :return: Return a dict with the current usage report + """ + resource = _make_request(config.get('api')) + return _extract_usage(resource, config.get('usage_metrics', {})) + + +def _extract_usage(resource, usage_metric_names): + """Parse the response from API to the expected structure.""" + usage_metrics = {} + for dimensions in resource.get('dimensions'): + usage_metric_name = dimensions.get('dimension') + if usage_metric_name in usage_metric_names: + usage_metrics[usage_metric_name] = dimensions.get('count') + + return usage_metrics + + +def _make_request(url): + """Make a request to the API, returns response or raise exception.""" + for attempt in range(0, 5): + message = None + try: + response = requests.get(url) + except requests.exceptions.HTTPError as err: + message = f'Http Error: {err}' + except requests.exceptions.ConnectionError as err: + message = f'Error Connecting: {err}' + except requests.exceptions.Timeout as err: + message = f'Timeout Error: {err}' + except requests.exceptions.RequestException as err: + message = f'Request error: {err}' + except Exception as err: + message = f'Unexpected error: {err}' + + if not message: + break + + if message: + raise CSPBillingAdapterException(f'{message}') + + return response.json() diff --git a/requirements.txt b/requirements.txt index 0e91665..9e32eff 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,3 @@ csp-billing-adapter @ git+https://github.com/suse-enceladus/csp-billing-adapter@main pluggy +requests diff --git a/tests/unit/test_plugin.py b/tests/unit/test_plugin.py index 636bb00..e202c52 100644 --- a/tests/unit/test_plugin.py +++ b/tests/unit/test_plugin.py @@ -25,9 +25,13 @@ from pathlib import Path from tempfile import NamedTemporaryFile from unittest.mock import patch +from pytest import raises + +import requests from csp_billing_adapter.config import Config from csp_billing_adapter.adapter import get_plugin_manager +from csp_billing_adapter.exceptions import CSPBillingAdapterException from csp_billing_adapter_local.plugin import ( @@ -36,7 +40,8 @@ update_cache, update_csp_config, save_cache, - save_csp_config + save_csp_config, + get_usage_data ) config_file = 'tests/data/config.yaml' @@ -295,3 +300,47 @@ def test_local_csp_config_save(): ) assert get_csp_config(config=local_config) == test_data2 + + +def test_local_csp_usage_data(): + class MockResponse: + def __init__(self, json_data, status_code): + self.json_data = json_data + self.status_code = status_code + + def json(self): + return self.json_data + + def ok(self): + return self.status_code < 400 + + json_data = { + "dimensions": [ + {"dimension": "managed_node_count", "count": 42} + ] + } + status_code = 200 + with patch( + 'csp_billing_adapter_local.plugin.requests.get', + return_value=MockResponse(json_data, status_code) + + ): + response = get_usage_data(config=local_config) + assert response == {'managed_node_count': 42} + + +def test_local_csp_usage_data_errors(): + errors = [ + requests.exceptions.HTTPError('HTTP'), + requests.exceptions.ConnectionError('Connection'), + requests.exceptions.Timeout('Timeout'), + requests.exceptions.RequestException('Request'), + Exception('Exception') + ] + for error in errors: + with patch( + 'csp_billing_adapter_local.plugin.requests.get', + side_effect=error + ): + with raises(CSPBillingAdapterException): + get_usage_data(config=local_config)