From f8da9dab789c7e9b91f2d1e08d8e61a284d9066b Mon Sep 17 00:00:00 2001 From: Pedro Henrique Date: Thu, 21 Aug 2025 07:48:18 -0300 Subject: [PATCH] Add support to rating rules with start and end It was introduced the concept of start and end periods in Cloudkitty rating rules. Therefore to make Cloudkitty CLI compatible with the Cloudkitty REST API, we need to add those new available attributes in the CLI as well. Change-Id: I0cd9b61fa81232d235c959da551a7840465fae88 Signed-off-by: Pedro Henrique --- .../tests/functional/v1/test_hashmap.py | 51 ++++++++++++++++++- .../tests/functional/v1/test_pyscripts.py | 51 +++++++++++++++++-- .../tests/unit/v1/test_hashmap.py | 9 +++- .../tests/unit/v1/test_pyscripts.py | 3 +- cloudkittyclient/v1/rating/hashmap.py | 20 ++++++++ cloudkittyclient/v1/rating/hashmap_cli.py | 14 +++++ cloudkittyclient/v1/rating/pyscripts.py | 19 ++++++- cloudkittyclient/v1/rating/pyscripts_cli.py | 11 ++++ 8 files changed, 168 insertions(+), 10 deletions(-) diff --git a/cloudkittyclient/tests/functional/v1/test_hashmap.py b/cloudkittyclient/tests/functional/v1/test_hashmap.py index 7e9058b..206e58b 100644 --- a/cloudkittyclient/tests/functional/v1/test_hashmap.py +++ b/cloudkittyclient/tests/functional/v1/test_hashmap.py @@ -13,6 +13,9 @@ # License for the specific language governing permissions and limitations # under the License. # +from datetime import datetime +from datetime import timedelta + from cloudkittyclient.tests.functional import base @@ -136,13 +139,15 @@ class CkHashmapTest(base.BaseFunctionalTest): self.assertEqual(len(resp), 0) def test_create_get_update_delete_mapping_service(self): + future_date = datetime.now() + timedelta(days=1) + date_iso = future_date.isoformat() resp = self.runner('hashmap service create', params='testservice')[0] service_id = resp['Service ID'] self._services.append(service_id) # Create mapping resp = self.runner('hashmap mapping create', - params='-s {} 12'.format(service_id))[0] + params=f'-s {service_id} 12 --start {date_iso}')[0] mapping_id = resp['Mapping ID'] self._mappings.append(mapping_id) self.assertEqual(resp['Service ID'], service_id) @@ -173,6 +178,8 @@ class CkHashmapTest(base.BaseFunctionalTest): 'hashmap service delete', params=service_id, has_output=False) def test_create_get_update_delete_mapping_field(self): + future_date = datetime.now() + timedelta(days=1) + date_iso = future_date.isoformat() resp = self.runner('hashmap service create', params='testservice')[0] service_id = resp['Service ID'] self._services.append(service_id) @@ -185,7 +192,8 @@ class CkHashmapTest(base.BaseFunctionalTest): # Create mapping resp = self.runner( 'hashmap mapping create', - params='--field-id {} 12 --value testvalue'.format(field_id))[0] + params=f'--field-id {field_id} 12 --value ' + f'testvalue --start {date_iso}')[0] mapping_id = resp['Mapping ID'] self._mappings.append(service_id) self.assertEqual(resp['Field ID'], field_id) @@ -203,6 +211,45 @@ class CkHashmapTest(base.BaseFunctionalTest): params='--cost 10 {}'.format(mapping_id))[0] self.assertEqual(float(resp['Cost']), float(10)) + def test_create_get_update_delete_mapping_field_started(self): + resp = self.runner('hashmap service create', + params='testservice_date_started')[0] + service_id = resp['Service ID'] + self._services.append(service_id) + + resp = self.runner( + 'hashmap field create', + params='{} testfield_date_started'.format(service_id))[0] + field_id = resp['Field ID'] + self._fields.append(field_id) + + # Create mapping + resp = self.runner( + 'hashmap mapping create', + params=f'--field-id {field_id} 12 --value ' + f'testvalue')[0] + mapping_id = resp['Mapping ID'] + self._mappings.append(service_id) + self.assertEqual(resp['Field ID'], field_id) + self.assertEqual(float(resp['Cost']), float(12)) + self.assertEqual(resp['Value'], 'testvalue') + + # Get mapping + resp = self.runner( + 'hashmap mapping get', params=mapping_id)[0] + self.assertEqual(resp['Mapping ID'], mapping_id) + self.assertEqual(float(resp['Cost']), float(12)) + + # Should not be able to update a rule that is running (start < now) + try: + self.runner('hashmap mapping update', + params='--cost 10 {}'.format(mapping_id))[0] + except RuntimeError as e: + expected_error = ("You are allowed to update only the attribute " + "[end] as this rule is already running as it " + "started on ") + self.assertIn(expected_error, str(e)) + def test_group_mappings_get(self): # Service and group resp = self.runner('hashmap service create', params='testservice')[0] diff --git a/cloudkittyclient/tests/functional/v1/test_pyscripts.py b/cloudkittyclient/tests/functional/v1/test_pyscripts.py index 0f05427..203a8ab 100644 --- a/cloudkittyclient/tests/functional/v1/test_pyscripts.py +++ b/cloudkittyclient/tests/functional/v1/test_pyscripts.py @@ -13,6 +13,9 @@ # License for the specific language governing permissions and limitations # under the License. # +from datetime import datetime +from datetime import timedelta + from cloudkittyclient.tests.functional import base @@ -23,9 +26,12 @@ class CkPyscriptTest(base.BaseFunctionalTest): self.runner = self.cloudkitty def test_create_get_update_list_delete(self): + future_date = datetime.now() + timedelta(days=1) + date_iso = future_date.isoformat() # Create resp = self.runner( - 'pyscript create', params="testscript 'return 0'")[0] + 'pyscript create', params=f"testscript " + f"'return 0' --start {date_iso}")[0] script_id = resp['Script ID'] self.assertEqual(resp['Name'], 'testscript') @@ -37,8 +43,9 @@ class CkPyscriptTest(base.BaseFunctionalTest): # Update resp = self.runner( 'pyscript update', - params="-n newname -d 'return 1' {}".format(script_id))[0] - self.assertEqual(resp['Name'], 'newname') + params="-d 'return 1' {} --description " + "desc".format(script_id))[0] + self.assertEqual(resp['Script Description'], 'desc') self.assertEqual(resp['Script ID'], script_id) self.assertEqual(resp['Data'], 'return 1') @@ -46,13 +53,49 @@ class CkPyscriptTest(base.BaseFunctionalTest): resp = self.runner('pyscript list') self.assertEqual(len(resp), 1) resp = resp[0] - self.assertEqual(resp['Name'], 'newname') + self.assertEqual(resp['Script Description'], 'desc') self.assertEqual(resp['Script ID'], script_id) self.assertEqual(resp['Data'], 'return 1') # Delete self.runner('pyscript delete', params=script_id, has_output=False) + def test_create_get_update_list_delete_started(self): + # Create + resp = self.runner( + 'pyscript create', params="testscript_started " + "'return 0'")[0] + script_id = resp['Script ID'] + self.assertEqual(resp['Name'], 'testscript_started') + + # Get + resp = self.runner('pyscript get', params=script_id)[0] + self.assertEqual(resp['Name'], 'testscript_started') + self.assertEqual(resp['Script ID'], script_id) + + # Should not be able to update a rule that is running (start < now) + try: + self.runner( + 'pyscript update', + params="-d 'return 1' {} --description " + "desc".format(script_id))[0] + except RuntimeError as e: + expected_error = ("You are allowed to update only the attribute " + "[end] as this rule is already running as it " + "started on ") + self.assertIn(expected_error, str(e)) + + # List + resp = self.runner('pyscript list') + self.assertEqual(len(resp), 1) + resp = resp[0] + self.assertEqual(resp['Script Description'], None) + self.assertEqual(resp['Script ID'], script_id) + self.assertEqual(resp['Data'], 'return 0') + + # Delete + self.runner('pyscript delete', params=script_id, has_output=False) + class OSCPyscriptTest(CkPyscriptTest): diff --git a/cloudkittyclient/tests/unit/v1/test_hashmap.py b/cloudkittyclient/tests/unit/v1/test_hashmap.py index e5e509f..78e6e8d 100644 --- a/cloudkittyclient/tests/unit/v1/test_hashmap.py +++ b/cloudkittyclient/tests/unit/v1/test_hashmap.py @@ -110,7 +110,10 @@ class TestHashmap(base.BaseAPIEndpointTestCase): self.assertRaises(exc.ArgumentRequired, self.hashmap.get_mapping) def test_create_mapping(self): - kwargs = dict(cost=2, value='value', field_id='field_id') + kwargs = dict(cost=2, value='value', field_id='field_id', + name='name', start="2024-01-01", + end="2024-01-01", + description="description") body = dict( cost=kwargs.get('cost'), value=kwargs.get('value'), @@ -119,6 +122,10 @@ class TestHashmap(base.BaseAPIEndpointTestCase): group_id=kwargs.get('group_id'), tenant_id=kwargs.get('tenant_id'), type=kwargs.get('type') or 'flat', + start="2024-01-01", + end="2024-01-01", + description="description", + name='name' ) self.hashmap.create_mapping(**kwargs) self.api_client.post.assert_called_once_with( diff --git a/cloudkittyclient/tests/unit/v1/test_pyscripts.py b/cloudkittyclient/tests/unit/v1/test_pyscripts.py index be38488..acac7b7 100644 --- a/cloudkittyclient/tests/unit/v1/test_pyscripts.py +++ b/cloudkittyclient/tests/unit/v1/test_pyscripts.py @@ -38,7 +38,8 @@ class TestPyscripts(base.BaseAPIEndpointTestCase): self.assertRaises(exc.ArgumentRequired, self.pyscripts.get_script) def test_create_script(self): - kwargs = dict(name='name', data='data') + kwargs = dict(name='name', data='data', start=None, + end=None, description=None) self.pyscripts.create_script(**kwargs) self.api_client.post.assert_called_once_with( '/v1/rating/module_config/pyscripts/scripts/', json=kwargs) diff --git a/cloudkittyclient/v1/rating/hashmap.py b/cloudkittyclient/v1/rating/hashmap.py index ba56dc2..674f9c1 100644 --- a/cloudkittyclient/v1/rating/hashmap.py +++ b/cloudkittyclient/v1/rating/hashmap.py @@ -13,6 +13,8 @@ # License for the specific language governing permissions and limitations # under the License. # +import uuid + from cloudkittyclient.common import base from cloudkittyclient import exc @@ -171,6 +173,14 @@ class HashmapManager(base.BaseManager): :type type: str :param value: Value of the mapping :type value: str + :param name: Name of the mapping + :type name: str + :param start: Date the mapping starts being valid + :type start: str + :param end: Date the mapping stops being valid + :type end: str + :param description: Description of the mapping + :type description: str """ if kwargs.get('cost') is None: raise exc.ArgumentRequired("'cost' argument is required") @@ -196,6 +206,16 @@ class HashmapManager(base.BaseManager): tenant_id=kwargs.get('tenant_id'), type=kwargs.get('type') or 'flat', ) + if kwargs.get('description'): + body['description'] = kwargs.get('description') + if kwargs.get('start'): + body['start'] = kwargs.get('start') + if kwargs.get('end'): + body['end'] = kwargs.get('end') + if kwargs.get('name'): + body['name'] = kwargs.get('name') + else: + body['name'] = uuid.uuid4().hex[:24] url = self.get_url('mappings', kwargs) return self.api_client.post(url, json=body).json() diff --git a/cloudkittyclient/v1/rating/hashmap_cli.py b/cloudkittyclient/v1/rating/hashmap_cli.py index 0cbfeac..de94a14 100644 --- a/cloudkittyclient/v1/rating/hashmap_cli.py +++ b/cloudkittyclient/v1/rating/hashmap_cli.py @@ -257,6 +257,10 @@ class CliCreateMapping(lister.Lister): ('service_id', 'Service ID'), ('group_id', 'Group ID'), ('tenant_id', 'Project ID'), + ('name', 'Mapping Name'), + ('start', 'Mapping Start Date'), + ('end', 'Mapping End Date'), + ('Description', 'Mapping Description') ] def take_action(self, parsed_args): @@ -275,6 +279,11 @@ class CliCreateMapping(lister.Lister): parser.add_argument('-t', '--type', type=str, help='Mapping type') parser.add_argument('--value', type=str, help='Value') parser.add_argument('cost', type=float, help='Cost') + parser.add_argument('--name', type=str, help='Mapping Name') + parser.add_argument('--start', type=str, help='Mapping Start') + parser.add_argument('--end', type=str, help='Mapping End') + parser.add_argument('--description', type=str, + help='Mapping Description') return parser @@ -321,6 +330,11 @@ class CliUpdateMapping(lister.Lister): parser.add_argument('--value', type=str, help='Value') parser.add_argument('--cost', type=str, help='Cost') parser.add_argument('mapping_id', type=str, help='Mapping ID') + parser.add_argument('--name', type=str, help='Mapping Name') + parser.add_argument('--start', type=str, help='Mapping Start') + parser.add_argument('--end', type=str, help='Mapping End') + parser.add_argument('--description', type=str, + help='Mapping Description') return parser diff --git a/cloudkittyclient/v1/rating/pyscripts.py b/cloudkittyclient/v1/rating/pyscripts.py index 0fcb8a2..cb1e20c 100644 --- a/cloudkittyclient/v1/rating/pyscripts.py +++ b/cloudkittyclient/v1/rating/pyscripts.py @@ -50,13 +50,22 @@ class PyscriptManager(base.BaseManager): :type name: str :param data: Content of the script :type data: str + :param start: Date the script starts being valid + :type start: str + :param end: Date the script stops being valid + :type end: str + :param description: Description of the script + :type description: str """ for arg in ('name', 'data'): if not kwargs.get(arg): raise exc.ArgumentRequired( "'Argument {} is required.'".format(arg)) url = self.get_url('scripts', kwargs) - body = dict(name=kwargs['name'], data=kwargs['data']) + body = dict(name=kwargs['name'], data=kwargs['data'], + start=kwargs.get('start'), + end=kwargs.get('end'), + description=kwargs.get('description')) return self.api_client.post(url, json=body).json() def update_script(self, **kwargs): @@ -68,11 +77,17 @@ class PyscriptManager(base.BaseManager): :type name: str :param data: Content of the script :type data: str + :param start: Date the script starts being valid + :type start: str + :param end: Date the script stops being valid + :type end: str + :param description: Description of the script + :type description: str """ if not kwargs.get('script_id'): raise exc.ArgumentRequired("Argument 'script_id' is required.") script = self.get_script(script_id=kwargs['script_id']) - for key in ('name', 'data'): + for key in ('name', 'data', 'start', 'end', 'description'): if kwargs.get(key): script[key] = kwargs[key] script.pop('checksum', None) diff --git a/cloudkittyclient/v1/rating/pyscripts_cli.py b/cloudkittyclient/v1/rating/pyscripts_cli.py index 8ede8de..176ad41 100644 --- a/cloudkittyclient/v1/rating/pyscripts_cli.py +++ b/cloudkittyclient/v1/rating/pyscripts_cli.py @@ -26,6 +26,9 @@ class BaseScriptCli(lister.Lister): ('script_id', 'Script ID'), ('checksum', 'Checksum'), ('data', 'Data'), + ('start', 'Script Start Date'), + ('end', 'Script End Date'), + ('description', 'Script Description') ] @@ -82,6 +85,10 @@ class CliCreateScript(BaseScriptCli): parser = super(CliCreateScript, self).get_parser(prog_name) parser.add_argument('name', type=str, help='Script Name') parser.add_argument('data', type=str, help='Script Data or data file') + parser.add_argument('--start', type=str, help='Script Start') + parser.add_argument('--end', type=str, help='Script End') + parser.add_argument('--description', type=str, + help='Script Description') return parser @@ -107,6 +114,10 @@ class CliUpdateScript(BaseScriptCli): parser.add_argument('-n', '--name', type=str, help='Script Name') parser.add_argument('-d', '--data', type=str, help='Script Data or data file') + parser.add_argument('--start', type=str, help='Script Start') + parser.add_argument('--end', type=str, help='Script End') + parser.add_argument('--description', type=str, + help='Script Description') return parser