From 2a3dd279dc4e29e25b57c139ed554fab8e56fa00 Mon Sep 17 00:00:00 2001 From: Justin Ferrieu Date: Thu, 12 Sep 2019 07:40:28 +0000 Subject: [PATCH] Add support for GET /v2/dataframes API endpoint to the client Support for the ``GET /v2/dataframes`` endpoint has been added to the client. A new ``dataframes get`` CLI command is also available. Story: 2005890 Task: 36384 Depends-On: https://review.opendev.org/#/c/679636 Change-Id: Idfe93025e0f740906d0f53f33547c7746fc15169 --- .../tests/functional/v2/test_dataframes.py | 5 ++ .../tests/unit/v2/test_dataframes.py | 21 ++++++ cloudkittyclient/v2/dataframes.py | 27 +++++++ cloudkittyclient/v2/dataframes_cli.py | 74 +++++++++++++++++++ ...upport-v2-dataframes-be3a17271f3c7188.yaml | 5 ++ setup.cfg | 4 +- 6 files changed, 134 insertions(+), 2 deletions(-) create mode 100644 releasenotes/notes/add-support-v2-dataframes-be3a17271f3c7188.yaml diff --git a/cloudkittyclient/tests/functional/v2/test_dataframes.py b/cloudkittyclient/tests/functional/v2/test_dataframes.py index ff17788..d69d47d 100644 --- a/cloudkittyclient/tests/functional/v2/test_dataframes.py +++ b/cloudkittyclient/tests/functional/v2/test_dataframes.py @@ -162,6 +162,11 @@ class CkDataframesTest(base.BaseFunctionalTest): has_output=False, ) + def test_dataframes_get(self): + # TODO(jferrieu): functional tests will be added in another + # patch for `dataframes get` + pass + class OSCDataframesTest(CkDataframesTest): def __init__(self, *args, **kwargs): diff --git a/cloudkittyclient/tests/unit/v2/test_dataframes.py b/cloudkittyclient/tests/unit/v2/test_dataframes.py index cd0cbce..1aa324d 100644 --- a/cloudkittyclient/tests/unit/v2/test_dataframes.py +++ b/cloudkittyclient/tests/unit/v2/test_dataframes.py @@ -14,6 +14,8 @@ # import json +from collections import OrderedDict + from cloudkittyclient import exc from cloudkittyclient.tests.unit.v2 import base @@ -149,3 +151,22 @@ class TestDataframes(base.BaseAPIEndpointTestCase): self.assertRaises( exc.ArgumentRequired, self.dataframes.add_dataframes) + + def test_get_dataframes(self): + self.dataframes.get_dataframes() + self.api_client.get.assert_called_once_with('/v2/dataframes') + + def test_get_dataframes_with_pagination_args(self): + self.dataframes.get_dataframes(offset=10, limit=10) + try: + self.api_client.get.assert_called_once_with( + '/v2/dataframes?limit=10&offset=10') + except AssertionError: + self.api_client.get.assert_called_once_with( + '/v2/dataframes?offset=10&limit=10') + + def test_get_dataframes_filters(self): + self.dataframes.get_dataframes( + filters=OrderedDict([('one', 'two'), ('three', 'four')])) + self.api_client.get.assert_called_once_with( + '/v2/dataframes?filters=one%3Atwo%2Cthree%3Afour') diff --git a/cloudkittyclient/v2/dataframes.py b/cloudkittyclient/v2/dataframes.py index a16fade..5637ace 100644 --- a/cloudkittyclient/v2/dataframes.py +++ b/cloudkittyclient/v2/dataframes.py @@ -48,3 +48,30 @@ class DataframesManager(base.BaseManager): url, data=dataframes, ) + + def get_dataframes(self, **kwargs): + """Returns a paginated list of DataFrames. + + This support filters and datetime framing. + + :param offset: Index of the first dataframe that should be returned. + :type offset: int + :param limit: Maximal number of dataframes to return. + :type limit: int + :param filters: Optional dict of filters to select data on. + :type filters: dict + :param begin: Start of the period to gather data from + :type begin: datetime.datetime + :param end: End of the period to gather data from + :type end: datetime.datetime + """ + kwargs['filters'] = ','.join( + '{}:{}'.format(k, v) for k, v in + (kwargs.get('filters', None) or {}).items() + ) + + authorized_args = [ + 'offset', 'limit', 'filters', 'begin', 'end'] + + url = self.get_url(None, kwargs, authorized_args=authorized_args) + return self.api_client.get(url).json() diff --git a/cloudkittyclient/v2/dataframes_cli.py b/cloudkittyclient/v2/dataframes_cli.py index 92698f2..9dfad88 100644 --- a/cloudkittyclient/v2/dataframes_cli.py +++ b/cloudkittyclient/v2/dataframes_cli.py @@ -15,6 +15,8 @@ import argparse from cliff import command +from cliff import lister +from oslo_utils import timeutils from cloudkittyclient import utils @@ -40,3 +42,75 @@ class CliDataframesAdd(command.Command): utils.get_client_from_osc(self).dataframes.add_dataframes( dataframes=dataframes, ) + + +class CliDataframesGet(lister.Lister): + """Get dataframes from the storage backend.""" + columns = [ + ('begin', 'Begin'), + ('end', 'End'), + ('metric', 'Metric Type'), + ('unit', 'Unit'), + ('qty', 'Quantity'), + ('price', 'Price'), + ('groupby', 'Group By'), + ('metadata', 'Metadata'), + ] + + def get_parser(self, prog_name): + parser = super(CliDataframesGet, self).get_parser(prog_name) + + def filter_(elem): + if len(elem.split(':')) != 2: + raise TypeError + return str(elem) + + parser.add_argument('--offset', type=int, default=0, + help='Index of the first dataframe') + parser.add_argument('--limit', type=int, default=100, + help='Maximal number of dataframes') + parser.add_argument('--filter', type=filter_, action='append', + help="Optional filter, in 'key:value' format. Can " + "be specified several times.") + parser.add_argument('-b', '--begin', type=timeutils.parse_isotime, + help="Start of the period to query, in iso8601 " + "format. Example: 2019-05-01T00:00:00Z.") + parser.add_argument('-e', '--end', type=timeutils.parse_isotime, + help="End of the period to query, in iso8601 " + "format. Example: 2019-06-01T00:00:00Z.") + + return parser + + def take_action(self, parsed_args): + filters = dict(elem.split(':') for elem in (parsed_args.filter or [])) + + dataframes = utils.get_client_from_osc(self).dataframes.get_dataframes( + offset=parsed_args.offset, + limit=parsed_args.limit, + begin=parsed_args.begin, + end=parsed_args.end, + filters=filters, + ).get('dataframes', []) + + def format_(d): + return ' '.join([ + '{}="{}"'.format(k, v) for k, v in (d or {}).items()]) + + values = [] + for df in dataframes: + period = df['period'] + usage = df['usage'] + for metric_type, points in usage.items(): + for point in points: + values.append([ + period['begin'], + period['end'], + metric_type, + point['vol']['unit'], + point['vol']['qty'], + point['rating']['price'], + format_(point.get('groupby', {})), + format_(point.get('metadata', {})), + ]) + + return [col[1] for col in self.columns], values diff --git a/releasenotes/notes/add-support-v2-dataframes-be3a17271f3c7188.yaml b/releasenotes/notes/add-support-v2-dataframes-be3a17271f3c7188.yaml new file mode 100644 index 0000000..c275613 --- /dev/null +++ b/releasenotes/notes/add-support-v2-dataframes-be3a17271f3c7188.yaml @@ -0,0 +1,5 @@ +--- +features: + - | + Support for the ``GET /v2/dataframes`` endpoint has been added + to the client. A new ``dataframes get`` CLI command is also available. diff --git a/setup.cfg b/setup.cfg index e88a504..549499a 100644 --- a/setup.cfg +++ b/setup.cfg @@ -88,6 +88,7 @@ openstack.rating.v1 = rating_pyscript_delete = cloudkittyclient.v1.rating.pyscripts_cli:CliDeleteScript openstack.rating.v2 = + rating_dataframes_get = cloudkittyclient.v2.dataframes_cli:CliDataframesGet rating_dataframes_add = cloudkittyclient.v2.dataframes_cli:CliDataframesAdd rating_scope_state_get = cloudkittyclient.v2.scope_cli:CliScopeStateGet @@ -139,7 +140,6 @@ openstack.rating.v2 = rating_collector_state_get = cloudkittyclient.v1.collector_cli:CliCollectorGetState rating_collector_enable = cloudkittyclient.v1.collector_cli:CliCollectorEnable rating_collector_disable = cloudkittyclient.v1.collector_cli:CliCollectorDisable - rating_dataframes_get = cloudkittyclient.v1.storage_cli:CliGetDataframes rating_pyscript_create = cloudkittyclient.v1.rating.pyscripts_cli:CliCreateScript rating_pyscript_list = cloudkittyclient.v1.rating.pyscripts_cli:CliListScripts @@ -205,6 +205,7 @@ cloudkittyclient_v1 = cloudkittyclient_v2 = dataframes_add = cloudkittyclient.v2.dataframes_cli:CliDataframesAdd + dataframes_get = cloudkittyclient.v2.dataframes_cli:CliDataframesGet scope_state_get = cloudkittyclient.v2.scope_cli:CliScopeStateGet scope_state_reset = cloudkittyclient.v2.scope_cli:CliScopeStateReset @@ -256,7 +257,6 @@ cloudkittyclient_v2 = collector_state_get = cloudkittyclient.v1.collector_cli:CliCollectorGetState collector_enable = cloudkittyclient.v1.collector_cli:CliCollectorEnable collector_disable = cloudkittyclient.v1.collector_cli:CliCollectorDisable - dataframes_get = cloudkittyclient.v1.storage_cli:CliGetDataframes pyscript_create = cloudkittyclient.v1.rating.pyscripts_cli:CliCreateScript pyscript_list = cloudkittyclient.v1.rating.pyscripts_cli:CliListScripts