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 <phpm13@gmail.com>
This commit is contained in:
Pedro Henrique
2025-08-21 07:48:18 -03:00
parent c4711dfd16
commit f8da9dab78
8 changed files with 168 additions and 10 deletions

View File

@@ -13,6 +13,9 @@
# 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 datetime import datetime
from datetime import timedelta
from cloudkittyclient.tests.functional import base from cloudkittyclient.tests.functional import base
@@ -136,13 +139,15 @@ class CkHashmapTest(base.BaseFunctionalTest):
self.assertEqual(len(resp), 0) self.assertEqual(len(resp), 0)
def test_create_get_update_delete_mapping_service(self): 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] resp = self.runner('hashmap service create', params='testservice')[0]
service_id = resp['Service ID'] service_id = resp['Service ID']
self._services.append(service_id) self._services.append(service_id)
# Create mapping # Create mapping
resp = self.runner('hashmap mapping create', 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'] mapping_id = resp['Mapping ID']
self._mappings.append(mapping_id) self._mappings.append(mapping_id)
self.assertEqual(resp['Service ID'], service_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) 'hashmap service delete', params=service_id, has_output=False)
def test_create_get_update_delete_mapping_field(self): 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] resp = self.runner('hashmap service create', params='testservice')[0]
service_id = resp['Service ID'] service_id = resp['Service ID']
self._services.append(service_id) self._services.append(service_id)
@@ -185,7 +192,8 @@ class CkHashmapTest(base.BaseFunctionalTest):
# Create mapping # Create mapping
resp = self.runner( resp = self.runner(
'hashmap mapping create', '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'] mapping_id = resp['Mapping ID']
self._mappings.append(service_id) self._mappings.append(service_id)
self.assertEqual(resp['Field ID'], field_id) self.assertEqual(resp['Field ID'], field_id)
@@ -203,6 +211,45 @@ class CkHashmapTest(base.BaseFunctionalTest):
params='--cost 10 {}'.format(mapping_id))[0] params='--cost 10 {}'.format(mapping_id))[0]
self.assertEqual(float(resp['Cost']), float(10)) 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): def test_group_mappings_get(self):
# Service and group # Service and group
resp = self.runner('hashmap service create', params='testservice')[0] resp = self.runner('hashmap service create', params='testservice')[0]

View File

@@ -13,6 +13,9 @@
# 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 datetime import datetime
from datetime import timedelta
from cloudkittyclient.tests.functional import base from cloudkittyclient.tests.functional import base
@@ -23,9 +26,12 @@ class CkPyscriptTest(base.BaseFunctionalTest):
self.runner = self.cloudkitty self.runner = self.cloudkitty
def test_create_get_update_list_delete(self): def test_create_get_update_list_delete(self):
future_date = datetime.now() + timedelta(days=1)
date_iso = future_date.isoformat()
# Create # Create
resp = self.runner( 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'] script_id = resp['Script ID']
self.assertEqual(resp['Name'], 'testscript') self.assertEqual(resp['Name'], 'testscript')
@@ -37,8 +43,9 @@ class CkPyscriptTest(base.BaseFunctionalTest):
# Update # Update
resp = self.runner( resp = self.runner(
'pyscript update', 'pyscript update',
params="-n newname -d 'return 1' {}".format(script_id))[0] params="-d 'return 1' {} --description "
self.assertEqual(resp['Name'], 'newname') "desc".format(script_id))[0]
self.assertEqual(resp['Script Description'], 'desc')
self.assertEqual(resp['Script ID'], script_id) self.assertEqual(resp['Script ID'], script_id)
self.assertEqual(resp['Data'], 'return 1') self.assertEqual(resp['Data'], 'return 1')
@@ -46,13 +53,49 @@ class CkPyscriptTest(base.BaseFunctionalTest):
resp = self.runner('pyscript list') resp = self.runner('pyscript list')
self.assertEqual(len(resp), 1) self.assertEqual(len(resp), 1)
resp = resp[0] resp = resp[0]
self.assertEqual(resp['Name'], 'newname') self.assertEqual(resp['Script Description'], 'desc')
self.assertEqual(resp['Script ID'], script_id) self.assertEqual(resp['Script ID'], script_id)
self.assertEqual(resp['Data'], 'return 1') self.assertEqual(resp['Data'], 'return 1')
# Delete # Delete
self.runner('pyscript delete', params=script_id, has_output=False) 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): class OSCPyscriptTest(CkPyscriptTest):

View File

@@ -110,7 +110,10 @@ class TestHashmap(base.BaseAPIEndpointTestCase):
self.assertRaises(exc.ArgumentRequired, self.hashmap.get_mapping) self.assertRaises(exc.ArgumentRequired, self.hashmap.get_mapping)
def test_create_mapping(self): 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( body = dict(
cost=kwargs.get('cost'), cost=kwargs.get('cost'),
value=kwargs.get('value'), value=kwargs.get('value'),
@@ -119,6 +122,10 @@ class TestHashmap(base.BaseAPIEndpointTestCase):
group_id=kwargs.get('group_id'), group_id=kwargs.get('group_id'),
tenant_id=kwargs.get('tenant_id'), tenant_id=kwargs.get('tenant_id'),
type=kwargs.get('type') or 'flat', type=kwargs.get('type') or 'flat',
start="2024-01-01",
end="2024-01-01",
description="description",
name='name'
) )
self.hashmap.create_mapping(**kwargs) self.hashmap.create_mapping(**kwargs)
self.api_client.post.assert_called_once_with( self.api_client.post.assert_called_once_with(

View File

@@ -38,7 +38,8 @@ class TestPyscripts(base.BaseAPIEndpointTestCase):
self.assertRaises(exc.ArgumentRequired, self.pyscripts.get_script) self.assertRaises(exc.ArgumentRequired, self.pyscripts.get_script)
def test_create_script(self): 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.pyscripts.create_script(**kwargs)
self.api_client.post.assert_called_once_with( self.api_client.post.assert_called_once_with(
'/v1/rating/module_config/pyscripts/scripts/', json=kwargs) '/v1/rating/module_config/pyscripts/scripts/', json=kwargs)

View File

@@ -13,6 +13,8 @@
# License for the specific language governing permissions and limitations # License for the specific language governing permissions and limitations
# under the License. # under the License.
# #
import uuid
from cloudkittyclient.common import base from cloudkittyclient.common import base
from cloudkittyclient import exc from cloudkittyclient import exc
@@ -171,6 +173,14 @@ class HashmapManager(base.BaseManager):
:type type: str :type type: str
:param value: Value of the mapping :param value: Value of the mapping
:type value: str :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: if kwargs.get('cost') is None:
raise exc.ArgumentRequired("'cost' argument is required") raise exc.ArgumentRequired("'cost' argument is required")
@@ -196,6 +206,16 @@ class HashmapManager(base.BaseManager):
tenant_id=kwargs.get('tenant_id'), tenant_id=kwargs.get('tenant_id'),
type=kwargs.get('type') or 'flat', 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) url = self.get_url('mappings', kwargs)
return self.api_client.post(url, json=body).json() return self.api_client.post(url, json=body).json()

View File

@@ -257,6 +257,10 @@ class CliCreateMapping(lister.Lister):
('service_id', 'Service ID'), ('service_id', 'Service ID'),
('group_id', 'Group ID'), ('group_id', 'Group ID'),
('tenant_id', 'Project 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): 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('-t', '--type', type=str, help='Mapping type')
parser.add_argument('--value', type=str, help='Value') parser.add_argument('--value', type=str, help='Value')
parser.add_argument('cost', type=float, help='Cost') 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 return parser
@@ -321,6 +330,11 @@ class CliUpdateMapping(lister.Lister):
parser.add_argument('--value', type=str, help='Value') parser.add_argument('--value', type=str, help='Value')
parser.add_argument('--cost', type=str, help='Cost') parser.add_argument('--cost', type=str, help='Cost')
parser.add_argument('mapping_id', type=str, help='Mapping ID') 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 return parser

View File

@@ -50,13 +50,22 @@ class PyscriptManager(base.BaseManager):
:type name: str :type name: str
:param data: Content of the script :param data: Content of the script
:type data: str :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'): for arg in ('name', 'data'):
if not kwargs.get(arg): if not kwargs.get(arg):
raise exc.ArgumentRequired( raise exc.ArgumentRequired(
"'Argument {} is required.'".format(arg)) "'Argument {} is required.'".format(arg))
url = self.get_url('scripts', kwargs) 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() return self.api_client.post(url, json=body).json()
def update_script(self, **kwargs): def update_script(self, **kwargs):
@@ -68,11 +77,17 @@ class PyscriptManager(base.BaseManager):
:type name: str :type name: str
:param data: Content of the script :param data: Content of the script
:type data: str :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'): if not kwargs.get('script_id'):
raise exc.ArgumentRequired("Argument 'script_id' is required.") raise exc.ArgumentRequired("Argument 'script_id' is required.")
script = self.get_script(script_id=kwargs['script_id']) 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): if kwargs.get(key):
script[key] = kwargs[key] script[key] = kwargs[key]
script.pop('checksum', None) script.pop('checksum', None)

View File

@@ -26,6 +26,9 @@ class BaseScriptCli(lister.Lister):
('script_id', 'Script ID'), ('script_id', 'Script ID'),
('checksum', 'Checksum'), ('checksum', 'Checksum'),
('data', 'Data'), ('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 = super(CliCreateScript, self).get_parser(prog_name)
parser.add_argument('name', type=str, help='Script 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('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 return parser
@@ -107,6 +114,10 @@ class CliUpdateScript(BaseScriptCli):
parser.add_argument('-n', '--name', type=str, help='Script Name') parser.add_argument('-n', '--name', type=str, help='Script Name')
parser.add_argument('-d', '--data', type=str, parser.add_argument('-d', '--data', type=str,
help='Script Data or data file') 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 return parser