From d660bef8378e918cb36c7eb10588db3edaafb06e Mon Sep 17 00:00:00 2001 From: Justin Ferrieu Date: Thu, 18 Jul 2019 08:37:05 +0000 Subject: [PATCH] Add support for PUT /v2/scope API endpoint to the client This allows to reset the state of one or several scopes through the API via the client library and cli tool. Change-Id: I69ce9a1c2ee0d8a6dd191a39e5c843e0baa1290f Story: 2005395 Task: 30794 --- .../tests/functional/v2/test_scope.py | 6 ++ cloudkittyclient/tests/unit/v2/test_scope.py | 57 +++++++++++++++++++ cloudkittyclient/v2/scope.py | 52 +++++++++++++++++ cloudkittyclient/v2/scope_cli.py | 43 ++++++++++++++ setup.cfg | 4 ++ 5 files changed, 162 insertions(+) diff --git a/cloudkittyclient/tests/functional/v2/test_scope.py b/cloudkittyclient/tests/functional/v2/test_scope.py index 67210e0..52909a5 100644 --- a/cloudkittyclient/tests/functional/v2/test_scope.py +++ b/cloudkittyclient/tests/functional/v2/test_scope.py @@ -27,6 +27,12 @@ class CkScopeTest(base.BaseFunctionalTest): # the state of a scope through the client # resp = self.runner('scope state get') + def test_scope_state_reset(self): + return True + # FIXME(jferrieu): Uncomment and update this once there is a way to set + # the state of a scope through the client + # resp = self.runner('scope state reset') + class OSCScopeTest(CkScopeTest): diff --git a/cloudkittyclient/tests/unit/v2/test_scope.py b/cloudkittyclient/tests/unit/v2/test_scope.py index b78736e..2fcc961 100644 --- a/cloudkittyclient/tests/unit/v2/test_scope.py +++ b/cloudkittyclient/tests/unit/v2/test_scope.py @@ -12,7 +12,9 @@ # License for the specific language governing permissions and limitations # under the License. # +from cloudkittyclient import exc from cloudkittyclient.tests.unit.v2 import base +import datetime class TestScope(base.BaseAPIEndpointTestCase): @@ -29,3 +31,58 @@ class TestScope(base.BaseAPIEndpointTestCase): except AssertionError: self.api_client.get.assert_called_once_with( '/v2/scope?offset=10&limit=10') + + def test_reset_scope_with_args(self): + self.scope.reset_scope_state( + state=datetime.datetime(2019, 5, 7), + all_scopes=True) + self.api_client.put.assert_called_once_with( + '/v2/scope', + json={ + 'state': datetime.datetime(2019, 5, 7), + 'all_scopes': True, + }) + + def test_reset_scope_with_list_args(self): + self.scope.reset_scope_state( + state=datetime.datetime(2019, 5, 7), + scope_id=['id1', 'id2'], + all_scopes=False) + self.api_client.put.assert_called_once_with( + '/v2/scope', + json={ + 'state': datetime.datetime(2019, 5, 7), + 'scope_id': 'id1,id2', + }) + + def test_reset_scope_strips_none_and_false_args(self): + self.scope.reset_scope_state( + state=datetime.datetime(2019, 5, 7), + all_scopes=False, + scope_key=None, + scope_id=['id1', 'id2']) + self.api_client.put.assert_called_once_with( + '/v2/scope', + json={ + 'state': datetime.datetime(2019, 5, 7), + 'scope_id': 'id1,id2', + }) + + def test_reset_scope_with_no_args_raises_exc(self): + self.assertRaises( + exc.ArgumentRequired, + self.scope.reset_scope_state) + + def test_reset_scope_with_lacking_args_raises_exc(self): + self.assertRaises( + exc.ArgumentRequired, + self.scope.reset_scope_state, + state=datetime.datetime(2019, 5, 7)) + + def test_reset_scope_with_both_args_raises_exc(self): + self.assertRaises( + exc.InvalidArgumentError, + self.scope.reset_scope_state, + state=datetime.datetime(2019, 5, 7), + scope_id=['id1', 'id2'], + all_scopes=True) diff --git a/cloudkittyclient/v2/scope.py b/cloudkittyclient/v2/scope.py index 4f5c8f5..a3d1aa7 100644 --- a/cloudkittyclient/v2/scope.py +++ b/cloudkittyclient/v2/scope.py @@ -13,6 +13,7 @@ # under the License. # from cloudkittyclient.common import base +from cloudkittyclient import exc class ScopeManager(base.BaseManager): @@ -48,3 +49,54 @@ class ScopeManager(base.BaseManager): 'offset', 'limit', 'collector', 'fetcher', 'scope_id', 'scope_key'] url = self.get_url(None, kwargs, authorized_args=authorized_args) return self.api_client.get(url).json() + + def reset_scope_state(self, **kwargs): + """Returns nothing. + + Some optional filters can be provided. + The all_scopes and the scope_id options are mutually exclusive and one + must be provided. + + :param state: datetime object from which the state will be reset + :type state: datetime.datetime + :param all_scopes: Whether all scopes must be reset + :type all_scopes: bool + :param collector: Optional collector to filter on. + :type collector: str or list of str + :param fetcher: Optional fetcher to filter on. + :type fetcher: str or list of str + :param scope_id: Optional scope_id to filter on. + :type scope_id: str or list of str + :param scope_key: Optional scope_key to filter on. + :type scope_key: str or list of str + """ + + if not kwargs.get('state'): + raise exc.ArgumentRequired("'state' argument is required") + + if not kwargs.get('all_scopes') and not kwargs.get('scope_id'): + raise exc.ArgumentRequired( + "You must specify either 'scope_id' or 'all_scopes'") + + if kwargs.get('all_scopes') and kwargs.get('scope_id'): + raise exc.InvalidArgumentError( + "You can't specify both 'scope_id' and 'all_scopes'") + + for key in ('collector', 'fetcher', 'scope_id', 'scope_key'): + if key in kwargs.keys(): + if isinstance(kwargs[key], list): + kwargs[key] = ','.join(kwargs[key]) + + body = dict( + state=kwargs.get('state'), + scope_id=kwargs.get('scope_id'), + scope_key=kwargs.get('scope_key'), + collector=kwargs.get('collector'), + fetcher=kwargs.get('fetcher'), + all_scopes=kwargs.get('all_scopes'), + ) + # Stripping None and False values + body = dict(filter(lambda elem: bool(elem[1]), body.items())) + + url = self.get_url(None, kwargs) + return self.api_client.put(url, json=body) diff --git a/cloudkittyclient/v2/scope_cli.py b/cloudkittyclient/v2/scope_cli.py index 2b29def..96015d3 100644 --- a/cloudkittyclient/v2/scope_cli.py +++ b/cloudkittyclient/v2/scope_cli.py @@ -12,7 +12,9 @@ # License for the specific language governing permissions and limitations # under the License. # +from cliff import command from cliff import lister +from oslo_utils import timeutils from cloudkittyclient import utils @@ -53,3 +55,44 @@ class CliScopeStateGet(lister.Lister): ) values = utils.list_to_cols(resp['results'], self.info_columns) return [col[1] for col in self.info_columns], values + + +class CliScopeStateReset(command.Command): + """Reset the state of several scopes.""" + info_columns = [ + ('scope_id', 'Scope ID'), + ('scope_key', 'Scope Key'), + ('collector', 'Collector'), + ('fetcher', 'Fetcher'), + ] + + def get_parser(self, prog_name): + parser = super(CliScopeStateReset, self).get_parser(prog_name) + + for col in self.info_columns: + parser.add_argument( + '--' + col[0].replace('_', '-'), type=str, + action='append', help='Optional filter on ' + col[1]) + + parser.add_argument( + '-a', '--all-scopes', + action='store_true', + help="Target all scopes at once") + + parser.add_argument( + 'state', + type=timeutils.parse_isotime, + help="State iso8601 datetime to which the state should be set. " + "Example: 2019-06-01T00:00:00Z.") + + return parser + + def take_action(self, parsed_args): + utils.get_client_from_osc(self).scope.reset_scope_state( + collector=parsed_args.collector, + fetcher=parsed_args.fetcher, + scope_id=parsed_args.scope_id, + scope_key=parsed_args.scope_key, + all_scopes=parsed_args.all_scopes, + state=parsed_args.state, + ) diff --git a/setup.cfg b/setup.cfg index 40862c1..ac76d92 100644 --- a/setup.cfg +++ b/setup.cfg @@ -87,6 +87,8 @@ openstack.rating.v1 = openstack.rating.v2 = rating_scope_state_get = cloudkittyclient.v2.scope_cli:CliScopeStateGet + rating_scope_state_reset = cloudkittyclient.v2.scope_cli:CliScopeStateReset + rating_summary_get = cloudkittyclient.v2.summary_cli:CliSummaryGet rating_report_tenant_list = cloudkittyclient.v1.report_cli:CliTenantList @@ -199,6 +201,8 @@ cloudkittyclient.v1 = cloudkittyclient.v2 = scope_state_get = cloudkittyclient.v2.scope_cli:CliScopeStateGet + scope_state_reset = cloudkittyclient.v2.scope_cli:CliScopeStateReset + summary_get = cloudkittyclient.v2.summary_cli:CliSummaryGet report_tenant_list = cloudkittyclient.v1.report_cli:CliTenantList