Merge "Introduce start and end dates on rating rules"
This commit is contained in:
@@ -13,10 +13,17 @@
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
#
|
||||
import datetime
|
||||
|
||||
from oslo_log import log as logging
|
||||
from oslo_utils import uuidutils
|
||||
from wsme.rest.json import tojson
|
||||
from wsme import types as wtypes
|
||||
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class UuidType(wtypes.UuidType):
|
||||
"""A simple UUID type."""
|
||||
basetype = wtypes.text
|
||||
@@ -29,6 +36,43 @@ class UuidType(wtypes.UuidType):
|
||||
return value
|
||||
|
||||
|
||||
class EndDayDatetimeBaseType(datetime.datetime):
|
||||
pass
|
||||
|
||||
|
||||
@tojson.when_object(EndDayDatetimeBaseType)
|
||||
def datetime_end_day_tojson(datatype, value):
|
||||
if value is None:
|
||||
return None
|
||||
return value.isoformat()
|
||||
|
||||
|
||||
class EndDayDatetime(wtypes.UserType):
|
||||
basetype = EndDayDatetimeBaseType
|
||||
name = 'end'
|
||||
|
||||
def validate(self, value):
|
||||
if isinstance(value, datetime.datetime):
|
||||
return value
|
||||
|
||||
token_that_splits_date_from_time_in_iso_format = 'T'
|
||||
|
||||
if token_that_splits_date_from_time_in_iso_format in value:
|
||||
LOG.debug("There is a time in the end date [%s]; "
|
||||
"therefore, we will maintain the time, "
|
||||
"and use the datetime as is.", value)
|
||||
|
||||
return datetime.datetime.fromisoformat(value)
|
||||
|
||||
LOG.debug("The end date [%s] was not defined with a specific time, "
|
||||
"using time [23:59:59] as end time.", value)
|
||||
|
||||
dt = datetime.datetime.fromisoformat(value)
|
||||
return datetime.datetime(
|
||||
year=dt.year, month=dt.month, day=dt.day,
|
||||
hour=23, minute=59, second=59)
|
||||
|
||||
|
||||
# Code taken from ironic types
|
||||
class MultiType(wtypes.UserType):
|
||||
"""A complex type that represents one or more types.
|
||||
|
||||
@@ -13,10 +13,13 @@
|
||||
# under the License.
|
||||
#
|
||||
import logging
|
||||
import pecan
|
||||
import requests
|
||||
|
||||
from keystoneauth1 import loading as ks_loading
|
||||
from keystoneauth1 import session as ks_session
|
||||
|
||||
from keystoneclient.v3 import client as ks_client
|
||||
from oslo_config import cfg
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
@@ -30,3 +33,26 @@ def create_custom_session(session_options, pool_size):
|
||||
pool_maxsize=pool_size)
|
||||
|
||||
return ks_session.Session(session=session, **session_options)
|
||||
|
||||
|
||||
def get_request_user():
|
||||
conf = cfg.CONF
|
||||
ks_auth = ks_loading.load_auth_from_conf_options(
|
||||
conf, 'keystone_authtoken')
|
||||
session = create_custom_session(
|
||||
{'auth': ks_auth, 'verify': False}, 1)
|
||||
|
||||
keystone_client = ks_client.Client(
|
||||
session=session,
|
||||
interface=conf['keystone_authtoken'].interface)
|
||||
|
||||
keystone_token = pecan.request.headers.get('X-Auth-Token')
|
||||
if not keystone_token:
|
||||
LOG.debug("There is no auth token in the request header, using "
|
||||
"'unknown' as the request user.")
|
||||
return 'unknown'
|
||||
token_data = ks_client.tokens.TokenManager(
|
||||
keystone_client).get_token_data(
|
||||
keystone_token)
|
||||
session.session.close()
|
||||
return token_data['token']['user']['id']
|
||||
|
||||
@@ -292,9 +292,21 @@ class Worker(BaseWorker):
|
||||
self._state = state.StateManager()
|
||||
self.next_timestamp_to_process = functools.partial(
|
||||
_check_state, self, self._period, self._tenant_id)
|
||||
|
||||
self.refresh_rating_rules()
|
||||
super(Worker, self).__init__(self._tenant_id)
|
||||
|
||||
def refresh_rating_rules(self):
|
||||
for processor in self._processors:
|
||||
processing_date = self.next_timestamp_to_process()
|
||||
processor.obj.reload_config(processing_date)
|
||||
data = getattr(processor.obj, '_entries',
|
||||
getattr(processor.obj, '_script',
|
||||
None))
|
||||
LOG.debug("Reloading rating rules for processor [%s]"
|
||||
" and scope [%s] at [%s] using rules [%s]",
|
||||
processor.obj.module_name,
|
||||
self._tenant_id, processing_date, data)
|
||||
|
||||
def _collect(self, metric, start_timestamp):
|
||||
next_timestamp = tzutils.add_delta(
|
||||
start_timestamp, timedelta(seconds=self._period))
|
||||
@@ -376,6 +388,7 @@ class Worker(BaseWorker):
|
||||
def run(self):
|
||||
should_continue_processing = self.execute_worker_processing()
|
||||
while should_continue_processing:
|
||||
self.refresh_rating_rules()
|
||||
should_continue_processing = self.execute_worker_processing()
|
||||
|
||||
def execute_worker_processing(self):
|
||||
|
||||
@@ -123,7 +123,7 @@ class RatingProcessorBase(object, metaclass=abc.ABCMeta):
|
||||
"""
|
||||
|
||||
@abc.abstractmethod
|
||||
def reload_config(self):
|
||||
def reload_config(self, start=None):
|
||||
"""Trigger configuration reload
|
||||
|
||||
"""
|
||||
|
||||
0
cloudkitty/rating/common/datamodels/__init__.py
Normal file
0
cloudkitty/rating/common/datamodels/__init__.py
Normal file
66
cloudkitty/rating/common/datamodels/models.py
Normal file
66
cloudkitty/rating/common/datamodels/models.py
Normal file
@@ -0,0 +1,66 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
#
|
||||
import datetime
|
||||
|
||||
from wsme import types as wtypes
|
||||
|
||||
from cloudkitty.api.v1 import types as ck_types
|
||||
|
||||
|
||||
class VolatileAuditableModel(wtypes.Base):
|
||||
|
||||
created_at = wtypes.wsattr(datetime.datetime, mandatory=False,
|
||||
default=None)
|
||||
"""The date the rule was created."""
|
||||
|
||||
start = wtypes.wsattr(datetime.datetime, mandatory=False, default=None)
|
||||
"""Must be None or a date in the future. To set a date in the past,
|
||||
use the force parameter in the POST query."""
|
||||
|
||||
end = wtypes.wsattr(ck_types.EndDayDatetime(), mandatory=False,
|
||||
default=None)
|
||||
"""Must be None or a date in the future. To set a date in the past,
|
||||
use the force parameter in the POST query."""
|
||||
|
||||
name = wtypes.wsattr(wtypes.text, mandatory=False, default=None)
|
||||
"""The name of the rule."""
|
||||
|
||||
description = wtypes.wsattr(wtypes.text, mandatory=False, default=None)
|
||||
"""The description of the rule."""
|
||||
|
||||
deleted = wtypes.wsattr(datetime.datetime, mandatory=False, default=None)
|
||||
"""The date the rule was deleted."""
|
||||
|
||||
created_by = wtypes.wsattr(wtypes.text, mandatory=False, default=None)
|
||||
"""The id of the user who created the rule."""
|
||||
|
||||
updated_by = wtypes.wsattr(wtypes.text, mandatory=False, default=None)
|
||||
"""The id of the user who last updated the rule."""
|
||||
|
||||
deleted_by = wtypes.wsattr(wtypes.text, mandatory=False, default=None)
|
||||
"""The id of the user who deleted the rule."""
|
||||
|
||||
@classmethod
|
||||
def sample(cls):
|
||||
sample = cls(created_at=datetime.datetime(2023, 1, 1, 10, 10, 10),
|
||||
start=datetime.datetime(2023, 2, 1),
|
||||
end=datetime.datetime(2023, 3, 1),
|
||||
name='rule 1',
|
||||
description='description',
|
||||
deleted=datetime.datetime(2023, 1, 15),
|
||||
created_by='7977999e2e2511e6a8b2df30b233ffcb',
|
||||
updated_by='7977999e2e2511e6a8b2df30b233ffcb',
|
||||
deleted_by='7977999e2e2511e6a8b2df30b233ffcb')
|
||||
return sample
|
||||
0
cloudkitty/rating/common/db/__init__.py
Normal file
0
cloudkitty/rating/common/db/__init__.py
Normal file
53
cloudkitty/rating/common/db/filters.py
Normal file
53
cloudkitty/rating/common/db/filters.py
Normal file
@@ -0,0 +1,53 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
#
|
||||
import datetime
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
def get_filters(query, model, deleted=False, start=None, end=None,
|
||||
updated_by=None, created_by=None, deleted_by=None,
|
||||
description=None, is_active=None):
|
||||
|
||||
if is_active:
|
||||
if not isinstance(is_active, bool):
|
||||
now = is_active
|
||||
else:
|
||||
now = datetime.datetime.now()
|
||||
query = query.filter(model.start <= now)
|
||||
query = query.filter(sa.or_(model.end > now, model.end == sa.null()))
|
||||
query = query.filter(model.deleted == sa.null())
|
||||
|
||||
if description:
|
||||
query = query.filter(model.description.ilike(f'%{description}%'))
|
||||
|
||||
if deleted_by:
|
||||
query = query.filter(model.deleted_by == deleted_by)
|
||||
|
||||
if created_by:
|
||||
query = query.filter(model.created_by == created_by)
|
||||
|
||||
if updated_by:
|
||||
query = query.filter(model.updated_by == updated_by)
|
||||
|
||||
if not deleted:
|
||||
query = query.filter(model.deleted == sa.null())
|
||||
|
||||
if start:
|
||||
query = query.filter(model.start >= start)
|
||||
|
||||
if end:
|
||||
query = query.filter(model.end < end)
|
||||
|
||||
return query
|
||||
63
cloudkitty/rating/common/db/migrations.py
Normal file
63
cloudkitty/rating/common/db/migrations.py
Normal file
@@ -0,0 +1,63 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
#
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
def create_common_tables(batch_op):
|
||||
batch_op.add_column(
|
||||
sa.Column(
|
||||
'created_at',
|
||||
sa.DateTime(),
|
||||
nullable=False,
|
||||
server_default=sa.sql.func.now()
|
||||
))
|
||||
batch_op.add_column(
|
||||
sa.Column(
|
||||
'start',
|
||||
sa.DateTime(),
|
||||
nullable=False,
|
||||
server_default=sa.sql.func.now()
|
||||
))
|
||||
batch_op.add_column(
|
||||
sa.Column(
|
||||
'end',
|
||||
sa.DateTime(),
|
||||
nullable=True))
|
||||
|
||||
batch_op.add_column(
|
||||
sa.Column(
|
||||
'description',
|
||||
sa.Text(length=256),
|
||||
nullable=True))
|
||||
batch_op.add_column(
|
||||
sa.Column(
|
||||
'deleted',
|
||||
sa.DateTime(),
|
||||
nullable=True))
|
||||
batch_op.add_column(
|
||||
sa.Column(
|
||||
'created_by',
|
||||
sa.String(length=32),
|
||||
nullable=False))
|
||||
batch_op.add_column(
|
||||
sa.Column(
|
||||
'updated_by',
|
||||
sa.String(length=32),
|
||||
nullable=True))
|
||||
batch_op.add_column(
|
||||
sa.Column(
|
||||
'deleted_by',
|
||||
sa.String(length=32),
|
||||
nullable=True))
|
||||
60
cloudkitty/rating/common/db/models.py
Normal file
60
cloudkitty/rating/common/db/models.py
Normal file
@@ -0,0 +1,60 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
#
|
||||
import datetime
|
||||
import sqlalchemy
|
||||
|
||||
|
||||
class VolatileAuditableModel:
|
||||
|
||||
created_at = sqlalchemy.Column(
|
||||
'created_at',
|
||||
sqlalchemy.DateTime(),
|
||||
nullable=False,
|
||||
default=datetime.datetime.now()
|
||||
)
|
||||
start = sqlalchemy.Column(
|
||||
'start',
|
||||
sqlalchemy.DateTime(),
|
||||
nullable=False,
|
||||
default=datetime.datetime.now()
|
||||
)
|
||||
end = sqlalchemy.Column(
|
||||
'end',
|
||||
sqlalchemy.DateTime(),
|
||||
nullable=True)
|
||||
name = sqlalchemy.Column(
|
||||
'name',
|
||||
sqlalchemy.String(length=32),
|
||||
nullable=False)
|
||||
description = sqlalchemy.Column(
|
||||
'description',
|
||||
sqlalchemy.String(length=256),
|
||||
nullable=True)
|
||||
deleted = sqlalchemy.Column(
|
||||
'deleted',
|
||||
sqlalchemy.DateTime(),
|
||||
nullable=True)
|
||||
created_by = sqlalchemy.Column(
|
||||
'created_by',
|
||||
sqlalchemy.String(length=32),
|
||||
nullable=False)
|
||||
updated_by = sqlalchemy.Column(
|
||||
'updated_by',
|
||||
sqlalchemy.String(length=32),
|
||||
nullable=True)
|
||||
deleted_by = sqlalchemy.Column(
|
||||
'deleted_by',
|
||||
sqlalchemy.String(length=32),
|
||||
nullable=True)
|
||||
0
cloudkitty/rating/common/validations/__init__.py
Normal file
0
cloudkitty/rating/common/validations/__init__.py
Normal file
87
cloudkitty/rating/common/validations/fields.py
Normal file
87
cloudkitty/rating/common/validations/fields.py
Normal file
@@ -0,0 +1,87 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
#
|
||||
import datetime
|
||||
import pecan
|
||||
|
||||
from oslo_log import log as logging
|
||||
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _resource_changed(current_resource, resource, ignore_attribute_check=None):
|
||||
if not ignore_attribute_check:
|
||||
ignore_attribute_check = []
|
||||
resource_attributes = resource._wsme_attributes
|
||||
for attribute in resource_attributes:
|
||||
new_value = getattr(resource, attribute.key, None)
|
||||
old_value = getattr(current_resource, attribute.key, None)
|
||||
if attribute.key in ignore_attribute_check:
|
||||
continue
|
||||
|
||||
if new_value and new_value != old_value:
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
|
||||
def validate_update_allowing_only_end_date(current_resource, resource):
|
||||
now = datetime.datetime.now()
|
||||
if current_resource.start < now:
|
||||
if current_resource.end is None:
|
||||
if _resource_changed(current_resource, resource,
|
||||
ignore_attribute_check=["end"]):
|
||||
pecan.abort(
|
||||
400, f'You are allowed to update '
|
||||
f'only the attribute [end] as this rule is '
|
||||
f'already running as it started on '
|
||||
f'[{current_resource.start}]')
|
||||
|
||||
if resource.end and resource.end < now:
|
||||
pecan.abort(
|
||||
400, f'End date must be in the future. '
|
||||
f'end=[{resource.end}] current time=[{now}]')
|
||||
return True
|
||||
|
||||
pecan.abort(
|
||||
400, 'Cannot update a rule that was already processed and '
|
||||
'has a defined end date.')
|
||||
|
||||
else:
|
||||
LOG.debug("Updating the rating rule [%s] with new data [%s] as it has "
|
||||
"not been used yet.", current_resource, resource)
|
||||
|
||||
resource.name = None
|
||||
if not resource.start:
|
||||
resource.start = current_resource.start
|
||||
validate_resource(resource)
|
||||
return False
|
||||
|
||||
|
||||
def validate_resource(resource, force=False):
|
||||
start = resource.start
|
||||
end = resource.end
|
||||
now = datetime.datetime.now()
|
||||
if not force and start and start < now:
|
||||
pecan.abort(
|
||||
400, f'Cannot create a rule with start in the past. '
|
||||
f'start=[{start}] current time=[{now}].')
|
||||
if end and end < start:
|
||||
pecan.abort(
|
||||
400, f'Cannot create a rule with start after end. '
|
||||
f'start=[{start}] end=[{end}].')
|
||||
if force:
|
||||
LOG.info("Creating resource [%s] at [%s] with start date [%s] using "
|
||||
"the flag 'Force'.", resource, now, start)
|
||||
@@ -41,11 +41,11 @@ class HashMap(rating.RatingProcessorBase):
|
||||
self._res = {}
|
||||
self._load_rates()
|
||||
|
||||
def reload_config(self):
|
||||
def reload_config(self, start=None):
|
||||
"""Reload the module's configuration.
|
||||
|
||||
"""
|
||||
self._load_rates()
|
||||
self._load_rates(start)
|
||||
|
||||
def _load_mappings(self, mappings_uuid_list):
|
||||
hashmap = hash_db_api.get_instance()
|
||||
@@ -93,13 +93,15 @@ class HashMap(rating.RatingProcessorBase):
|
||||
root,
|
||||
service_uuid=None,
|
||||
field_uuid=None,
|
||||
tenant_uuid=None):
|
||||
tenant_uuid=None,
|
||||
start=None):
|
||||
hashmap = hash_db_api.get_instance()
|
||||
list_func = getattr(hashmap, 'list_{}'.format(entry_type))
|
||||
entries_uuid_list = list_func(
|
||||
service_uuid=service_uuid,
|
||||
field_uuid=field_uuid,
|
||||
tenant_uuid=tenant_uuid)
|
||||
tenant_uuid=tenant_uuid,
|
||||
is_active=start or True)
|
||||
load_func = getattr(self, '_load_{}'.format(entry_type))
|
||||
entries = load_func(entries_uuid_list)
|
||||
if entry_type in root:
|
||||
@@ -112,7 +114,7 @@ class HashMap(rating.RatingProcessorBase):
|
||||
else:
|
||||
root[entry_type] = entries
|
||||
|
||||
def _load_service_entries(self, service_name, service_uuid):
|
||||
def _load_service_entries(self, service_name, service_uuid, start=None):
|
||||
self._entries[service_name] = dict()
|
||||
for entry_type in ('mappings', 'thresholds'):
|
||||
for tenant in (None, self._tenant_id):
|
||||
@@ -120,9 +122,11 @@ class HashMap(rating.RatingProcessorBase):
|
||||
entry_type,
|
||||
self._entries[service_name],
|
||||
service_uuid=service_uuid,
|
||||
tenant_uuid=tenant)
|
||||
tenant_uuid=tenant,
|
||||
start=start)
|
||||
|
||||
def _load_field_entries(self, service_name, field_name, field_uuid):
|
||||
def _load_field_entries(self, service_name, field_name, field_uuid,
|
||||
start=None):
|
||||
if service_name not in self._entries:
|
||||
self._entries[service_name] = {}
|
||||
if 'fields' not in self._entries[service_name]:
|
||||
@@ -134,21 +138,23 @@ class HashMap(rating.RatingProcessorBase):
|
||||
entry_type,
|
||||
scope,
|
||||
field_uuid=field_uuid,
|
||||
tenant_uuid=tenant)
|
||||
tenant_uuid=tenant,
|
||||
start=start)
|
||||
|
||||
def _load_rates(self):
|
||||
def _load_rates(self, start=None):
|
||||
self._entries = {}
|
||||
hashmap = hash_db_api.get_instance()
|
||||
services_uuid_list = hashmap.list_services()
|
||||
for service_uuid in services_uuid_list:
|
||||
service_db = hashmap.get_service(uuid=service_uuid)
|
||||
service_name = service_db.name
|
||||
self._load_service_entries(service_name, service_uuid)
|
||||
self._load_service_entries(service_name, service_uuid, start)
|
||||
fields_uuid_list = hashmap.list_fields(service_uuid)
|
||||
for field_uuid in fields_uuid_list:
|
||||
field_db = hashmap.get_field(uuid=field_uuid)
|
||||
field_name = field_db.name
|
||||
self._load_field_entries(service_name, field_name, field_uuid)
|
||||
self._load_field_entries(service_name, field_name, field_uuid,
|
||||
start)
|
||||
|
||||
def add_rating_informations(self, point):
|
||||
for entry in self._res.values():
|
||||
|
||||
@@ -13,11 +13,14 @@
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
#
|
||||
import datetime
|
||||
import pecan
|
||||
import wsmeext.pecan as wsme_pecan
|
||||
|
||||
from cloudkitty.api.v1 import types as ck_types
|
||||
from cloudkitty.common.custom_session import get_request_user
|
||||
from cloudkitty import rating
|
||||
from cloudkitty.rating.common.validations import fields as field_validations
|
||||
from cloudkitty.rating.hash.datamodels import group as group_models
|
||||
from cloudkitty.rating.hash.datamodels import mapping as mapping_models
|
||||
from cloudkitty.rating.hash.db import api as db_api
|
||||
@@ -53,6 +56,15 @@ class HashMapMappingsController(rating.RatingRestControllerBase):
|
||||
bool,
|
||||
ck_types.UuidType(),
|
||||
bool,
|
||||
bool,
|
||||
datetime.datetime,
|
||||
datetime.datetime,
|
||||
str,
|
||||
str,
|
||||
str,
|
||||
str,
|
||||
bool,
|
||||
bool,
|
||||
status_code=200)
|
||||
def get_all(self,
|
||||
service_id=None,
|
||||
@@ -60,7 +72,16 @@ class HashMapMappingsController(rating.RatingRestControllerBase):
|
||||
group_id=None,
|
||||
no_group=False,
|
||||
tenant_id=None,
|
||||
filter_tenant=False):
|
||||
filter_tenant=False,
|
||||
deleted=False,
|
||||
start=None,
|
||||
end=None,
|
||||
updated_by=None,
|
||||
created_by=None,
|
||||
deleted_by=None,
|
||||
description=None,
|
||||
is_active=None,
|
||||
all=True):
|
||||
"""Get the mapping list
|
||||
|
||||
:param service_id: Service UUID to filter on.
|
||||
@@ -71,6 +92,15 @@ class HashMapMappingsController(rating.RatingRestControllerBase):
|
||||
:param filter_tenant: Explicitly filter on tenant (default is to not
|
||||
filter on tenant). Useful if you want to filter
|
||||
on tenant being None.
|
||||
:param deleted: Show deleted mappings.
|
||||
:param start: Mappings with start after date.
|
||||
:param end: Mappings with end before date.
|
||||
:param updated_by: user uuid to filter on.
|
||||
:param created_by: user uuid to filter on.
|
||||
:param deleted_by: user uuid to filter on.
|
||||
:param description: mapping that contains the text in description.
|
||||
:param is_active: only active mappings.
|
||||
:param: all: list all rules.
|
||||
:return: List of every mappings.
|
||||
"""
|
||||
hashmap = db_api.get_instance()
|
||||
@@ -83,6 +113,15 @@ class HashMapMappingsController(rating.RatingRestControllerBase):
|
||||
field_uuid=field_id,
|
||||
group_uuid=group_id,
|
||||
no_group=no_group,
|
||||
deleted=deleted,
|
||||
start=start,
|
||||
end=end,
|
||||
updated_by=updated_by,
|
||||
created_by=created_by,
|
||||
deleted_by=deleted_by,
|
||||
description=description,
|
||||
is_active=is_active,
|
||||
all=all,
|
||||
**search_opts)
|
||||
for mapping_uuid in mappings_uuid_list:
|
||||
mapping_db = hashmap.get_mapping(uuid=mapping_uuid)
|
||||
@@ -107,15 +146,20 @@ class HashMapMappingsController(rating.RatingRestControllerBase):
|
||||
pecan.abort(404, e.args[0])
|
||||
|
||||
@wsme_pecan.wsexpose(mapping_models.Mapping,
|
||||
bool,
|
||||
body=mapping_models.Mapping,
|
||||
status_code=201)
|
||||
def post(self, mapping_data):
|
||||
def post(self, force=False, mapping_data=None):
|
||||
"""Create a mapping.
|
||||
|
||||
:param force: Allows start and end in the past.
|
||||
:param mapping_data: Informations about the mapping to create.
|
||||
"""
|
||||
hashmap = db_api.get_instance()
|
||||
field_validations.validate_resource(
|
||||
mapping_data, force=force)
|
||||
try:
|
||||
created_by = get_request_user()
|
||||
mapping_db = hashmap.create_mapping(
|
||||
value=mapping_data.value,
|
||||
map_type=mapping_data.map_type,
|
||||
@@ -123,7 +167,12 @@ class HashMapMappingsController(rating.RatingRestControllerBase):
|
||||
field_id=mapping_data.field_id,
|
||||
group_id=mapping_data.group_id,
|
||||
service_id=mapping_data.service_id,
|
||||
tenant_id=mapping_data.tenant_id)
|
||||
tenant_id=mapping_data.tenant_id,
|
||||
created_by=created_by,
|
||||
start=mapping_data.start,
|
||||
end=mapping_data.end,
|
||||
name=mapping_data.name,
|
||||
description=mapping_data.description)
|
||||
pecan.response.location = pecan.request.path_url
|
||||
if pecan.response.location[-1] != '/':
|
||||
pecan.response.location += '/'
|
||||
@@ -147,14 +196,23 @@ class HashMapMappingsController(rating.RatingRestControllerBase):
|
||||
"""
|
||||
hashmap = db_api.get_instance()
|
||||
try:
|
||||
hashmap.update_mapping(
|
||||
mapping_id,
|
||||
mapping_id=mapping.mapping_id,
|
||||
value=mapping.value,
|
||||
cost=mapping.cost,
|
||||
map_type=mapping.map_type,
|
||||
group_id=mapping.group_id,
|
||||
tenant_id=mapping.tenant_id)
|
||||
updated_by = get_request_user()
|
||||
current_mapping = hashmap.get_mapping(mapping_id)
|
||||
if field_validations.validate_update_allowing_only_end_date(
|
||||
current_mapping,
|
||||
mapping):
|
||||
hashmap.update_mapping(
|
||||
mapping_id,
|
||||
end=mapping.end,
|
||||
updated_by=updated_by)
|
||||
else:
|
||||
hashmap.update_mapping(
|
||||
mapping_id,
|
||||
cost=mapping.cost,
|
||||
start=mapping.start,
|
||||
end=mapping.end,
|
||||
updated_by=updated_by,
|
||||
description=mapping.description)
|
||||
pecan.response.headers['Location'] = pecan.request.path
|
||||
except db_api.MappingAlreadyExists as e:
|
||||
pecan.abort(409, e.args[0])
|
||||
@@ -172,7 +230,8 @@ class HashMapMappingsController(rating.RatingRestControllerBase):
|
||||
:param mapping_id: UUID of the mapping to delete.
|
||||
"""
|
||||
hashmap = db_api.get_instance()
|
||||
deleted_by = get_request_user()
|
||||
try:
|
||||
hashmap.delete_mapping(uuid=mapping_id)
|
||||
hashmap.delete_mapping(mapping_id, deleted_by=deleted_by)
|
||||
except db_api.NoSuchMapping as e:
|
||||
pecan.abort(404, e.args[0])
|
||||
|
||||
@@ -18,11 +18,12 @@ import decimal
|
||||
from wsme import types as wtypes
|
||||
|
||||
from cloudkitty.api.v1 import types as ck_types
|
||||
from cloudkitty.rating.common.datamodels.models import VolatileAuditableModel
|
||||
|
||||
MAP_TYPE = wtypes.Enum(wtypes.text, 'flat', 'rate')
|
||||
|
||||
|
||||
class Mapping(wtypes.Base):
|
||||
class Mapping(VolatileAuditableModel):
|
||||
"""Type describing a Mapping.
|
||||
|
||||
A mapping is used to apply rating rules based on a value, if the parent is
|
||||
@@ -59,12 +60,14 @@ class Mapping(wtypes.Base):
|
||||
|
||||
@classmethod
|
||||
def sample(cls):
|
||||
sample = super().sample()
|
||||
sample = cls(mapping_id='39dbd39d-f663-4444-a795-fb19d81af136',
|
||||
field_id='ac55b000-a05b-4832-b2ff-265a034886ab',
|
||||
value='m1.micro',
|
||||
map_type='flat',
|
||||
cost=decimal.Decimal('4.2'),
|
||||
tenant_id='7977999e-2e25-11e6-a8b2-df30b233ffcb')
|
||||
tenant_id='7977999e-2e25-11e6-a8b2-df30b233ffcb',
|
||||
**sample.__dict__)
|
||||
return sample
|
||||
|
||||
|
||||
|
||||
@@ -235,6 +235,13 @@ class HashMap(object, metaclass=abc.ABCMeta):
|
||||
:param uuid: UUID of the mapping to get.
|
||||
"""
|
||||
|
||||
@abc.abstractmethod
|
||||
def get_mapping_by_name(self, name):
|
||||
"""Return a mapping object.
|
||||
|
||||
:param name: name of the mapping to get.
|
||||
"""
|
||||
|
||||
@abc.abstractmethod
|
||||
def get_threshold(self, uuid):
|
||||
"""Return a threshold object.
|
||||
@@ -327,7 +334,12 @@ class HashMap(object, metaclass=abc.ABCMeta):
|
||||
service_id=None,
|
||||
field_id=None,
|
||||
group_id=None,
|
||||
tenant_id=None):
|
||||
tenant_id=None,
|
||||
start=None,
|
||||
end=None,
|
||||
name=None,
|
||||
description=None,
|
||||
created_by=None):
|
||||
"""Create a new service/field mapping.
|
||||
|
||||
:param cost: Rating value to apply to this mapping.
|
||||
@@ -337,6 +349,11 @@ class HashMap(object, metaclass=abc.ABCMeta):
|
||||
:param field_id: Field the mapping is applying to.
|
||||
:param group_id: The group of calculations to apply.
|
||||
:param tenant_id: The tenant to apply calculations to.
|
||||
:param start: The date the rule will start to be valid.
|
||||
:param end: The date the rule will stop to be valid.
|
||||
:param name: The rule name.
|
||||
:param description: The rule description
|
||||
:param created_by: The user who created the rule.
|
||||
"""
|
||||
|
||||
@abc.abstractmethod
|
||||
@@ -404,10 +421,11 @@ class HashMap(object, metaclass=abc.ABCMeta):
|
||||
"""
|
||||
|
||||
@abc.abstractmethod
|
||||
def delete_mapping(self, uuid):
|
||||
def delete_mapping(self, uuid, deleted_by=None):
|
||||
"""Delete a mapping
|
||||
|
||||
:param uuid: UUID of the mapping to delete.
|
||||
:param deleted_by: UUID of the user who deleted the mapping.
|
||||
"""
|
||||
@abc.abstractmethod
|
||||
def delete_threshold(self, uuid):
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
#
|
||||
|
||||
"""Add start end dates and audit in hashmap mappings
|
||||
|
||||
Revision ID: 8a591f85865f
|
||||
Revises: 4e0232ce
|
||||
Create Date: 2023-03-06 14:22:00.000000
|
||||
|
||||
"""
|
||||
|
||||
from alembic import op
|
||||
from cloudkitty import db
|
||||
from cloudkitty.rating.common.db.migrations import create_common_tables
|
||||
from cloudkitty.rating.hash.db.sqlalchemy import models
|
||||
|
||||
import datetime
|
||||
import uuid
|
||||
|
||||
import sqlalchemy as sa
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = '8a591f85865f'
|
||||
down_revision = '4e0232ce'
|
||||
|
||||
|
||||
def _update_start_date():
|
||||
# Timestamp zero.
|
||||
initial_start_date = datetime.datetime(year=1970, month=1, day=1,
|
||||
tzinfo=datetime.timezone.utc)
|
||||
with db.session_for_write() as session:
|
||||
q = session.query(models.HashMapMapping)
|
||||
mapping_db = q.with_for_update().all()
|
||||
for entry in mapping_db:
|
||||
entry.start = initial_start_date
|
||||
entry.name = uuid.uuid4().hex.replace('-', '')
|
||||
entry.created_by = 'migration'
|
||||
|
||||
|
||||
def upgrade():
|
||||
table_name = 'hashmap_mappings'
|
||||
with op.batch_alter_table(
|
||||
table_name) as batch_op:
|
||||
# As we are not delete rows anymore, the constraint
|
||||
# validations will be delegated to the application.
|
||||
batch_op.drop_constraint(
|
||||
'uniq_field_mapping',
|
||||
type_='unique')
|
||||
batch_op.drop_constraint(
|
||||
'uniq_service_mapping',
|
||||
type_='unique')
|
||||
batch_op.add_column(
|
||||
sa.Column(
|
||||
'name',
|
||||
sa.String(length=32),
|
||||
nullable=False))
|
||||
create_common_tables(batch_op)
|
||||
|
||||
_update_start_date()
|
||||
@@ -13,12 +13,15 @@
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
#
|
||||
import datetime
|
||||
|
||||
from oslo_db import exception
|
||||
from oslo_db.sqlalchemy import utils
|
||||
from oslo_utils import uuidutils
|
||||
import sqlalchemy
|
||||
|
||||
from cloudkitty import db
|
||||
from cloudkitty.rating.common.db.filters import get_filters
|
||||
from cloudkitty.rating.hash.db import api
|
||||
from cloudkitty.rating.hash.db.sqlalchemy import migration
|
||||
from cloudkitty.rating.hash.db.sqlalchemy import models
|
||||
@@ -99,6 +102,18 @@ class HashMap(api.HashMap):
|
||||
except sqlalchemy.orm.exc.NoResultFound:
|
||||
raise api.NoSuchMapping(uuid)
|
||||
|
||||
def get_mapping_by_name(self, name):
|
||||
with db.session_for_read() as session:
|
||||
try:
|
||||
q = session.query(models.HashMapMapping)
|
||||
q = q.filter(
|
||||
models.HashMapMapping.deleted == sqlalchemy.null(),
|
||||
models.HashMapMapping.name == name)
|
||||
res = q.all()
|
||||
return res
|
||||
except sqlalchemy.orm.exc.NoResultFound:
|
||||
raise api.NoSuchMapping(name)
|
||||
|
||||
def get_threshold(self, uuid):
|
||||
with db.session_for_read() as session:
|
||||
try:
|
||||
@@ -165,6 +180,7 @@ class HashMap(api.HashMap):
|
||||
field_uuid=None,
|
||||
group_uuid=None,
|
||||
no_group=False,
|
||||
all=False,
|
||||
**kwargs):
|
||||
|
||||
with db.session_for_read() as session:
|
||||
@@ -178,13 +194,13 @@ class HashMap(api.HashMap):
|
||||
q = q.join(
|
||||
models.HashMapMapping.field)
|
||||
q = q.filter(models.HashMapField.field_id == field_uuid)
|
||||
elif not service_uuid and not field_uuid and not group_uuid:
|
||||
elif not (service_uuid or field_uuid or group_uuid) and not all:
|
||||
raise api.ClientHashMapError(
|
||||
'You must specify either service_uuid,'
|
||||
' field_uuid or group_uuid.')
|
||||
if 'tenant_uuid' in kwargs:
|
||||
q = q.filter(
|
||||
models.HashMapMapping.tenant_id == kwargs.get(
|
||||
models.HashMapMapping.tenant_id == kwargs.pop(
|
||||
'tenant_uuid'))
|
||||
if group_uuid:
|
||||
q = q.join(
|
||||
@@ -192,6 +208,8 @@ class HashMap(api.HashMap):
|
||||
q = q.filter(models.HashMapGroup.group_id == group_uuid)
|
||||
elif no_group:
|
||||
q = q.filter(models.HashMapMapping.group_id == None) # noqa
|
||||
|
||||
q = get_filters(q, models.HashMapMapping, **kwargs)
|
||||
res = q.values(
|
||||
models.HashMapMapping.mapping_id)
|
||||
return [uuid[0] for uuid in res]
|
||||
@@ -282,7 +300,14 @@ class HashMap(api.HashMap):
|
||||
service_id=None,
|
||||
field_id=None,
|
||||
group_id=None,
|
||||
tenant_id=None):
|
||||
tenant_id=None,
|
||||
start=None,
|
||||
end=None,
|
||||
name=None,
|
||||
description=None,
|
||||
created_by=None):
|
||||
if not name:
|
||||
name = uuidutils.generate_uuid(False)
|
||||
if field_id and service_id:
|
||||
raise api.ClientHashMapError('You can only specify one parent.')
|
||||
elif not service_id and not field_id:
|
||||
@@ -295,6 +320,11 @@ class HashMap(api.HashMap):
|
||||
raise api.ClientHashMapError(
|
||||
'You must specify a value'
|
||||
' for a field mapping.')
|
||||
|
||||
created_at = datetime.datetime.now()
|
||||
if not start:
|
||||
start = created_at
|
||||
|
||||
field_fk = None
|
||||
if field_id:
|
||||
field_db = self.get_field(uuid=field_id)
|
||||
@@ -309,14 +339,89 @@ class HashMap(api.HashMap):
|
||||
group_fk = group_db.id
|
||||
try:
|
||||
with db.session_for_write() as session:
|
||||
field_map = models.HashMapMapping(
|
||||
map_model = models.HashMapMapping
|
||||
q = session.query(map_model)
|
||||
or_ = sqlalchemy.or_
|
||||
and_ = sqlalchemy.and_
|
||||
null_ = sqlalchemy.null()
|
||||
filter_name = None
|
||||
filter_value = None
|
||||
if field_fk:
|
||||
filter_name = map_model.field_id
|
||||
filter_value = field_fk
|
||||
if service_fk:
|
||||
filter_name = map_model.service_id
|
||||
filter_value = service_fk
|
||||
|
||||
q = q.filter(
|
||||
map_model.deleted == null_)
|
||||
|
||||
if end:
|
||||
date_filter = and_(
|
||||
map_model.start < end,
|
||||
or_(
|
||||
map_model.end >= start,
|
||||
map_model.end == null_
|
||||
)
|
||||
)
|
||||
else:
|
||||
date_filter = or_(
|
||||
map_model.start > start,
|
||||
map_model.end == null_
|
||||
)
|
||||
|
||||
if filter_name:
|
||||
name_filter = or_(
|
||||
map_model.name == name,
|
||||
sqlalchemy.and_(
|
||||
date_filter,
|
||||
and_(
|
||||
map_model.value == value,
|
||||
map_model.tenant_id == tenant_id,
|
||||
filter_name == filter_value,
|
||||
map_model.group_id == group_fk
|
||||
)
|
||||
)
|
||||
)
|
||||
else:
|
||||
name_filter = or_(
|
||||
map_model.name == name,
|
||||
date_filter
|
||||
)
|
||||
|
||||
q = q.filter(name_filter)
|
||||
mapping_db = q.with_for_update().all()
|
||||
if mapping_db:
|
||||
if field_id:
|
||||
puuid = field_id
|
||||
ptype = 'field'
|
||||
else:
|
||||
puuid = service_id
|
||||
ptype = 'service'
|
||||
raise api.MappingAlreadyExists(
|
||||
value,
|
||||
puuid,
|
||||
ptype,
|
||||
tenant_id=tenant_id)
|
||||
|
||||
field_map = map_model(
|
||||
mapping_id=uuidutils.generate_uuid(),
|
||||
value=value,
|
||||
cost=cost,
|
||||
field_id=field_fk,
|
||||
service_id=service_fk,
|
||||
map_type=map_type,
|
||||
tenant_id=tenant_id)
|
||||
tenant_id=tenant_id,
|
||||
created_at=created_at,
|
||||
start=start,
|
||||
end=end,
|
||||
name=name,
|
||||
description=description,
|
||||
deleted=None,
|
||||
created_by=created_by,
|
||||
updated_by=None,
|
||||
deleted_by=None
|
||||
)
|
||||
if group_fk:
|
||||
field_map.group_id = group_fk
|
||||
session.add(field_map)
|
||||
@@ -396,7 +501,8 @@ class HashMap(api.HashMap):
|
||||
with db.session_for_write() as session:
|
||||
q = session.query(models.HashMapMapping)
|
||||
q = q.filter(
|
||||
models.HashMapMapping.mapping_id == uuid)
|
||||
models.HashMapMapping.mapping_id == uuid,
|
||||
models.HashMapMapping.deleted == sqlalchemy.null())
|
||||
mapping_db = q.with_for_update().one()
|
||||
if kwargs:
|
||||
# NOTE(sheeprine): We want to check that value is not set
|
||||
@@ -520,15 +626,18 @@ class HashMap(api.HashMap):
|
||||
session.delete(threshold)
|
||||
q.delete()
|
||||
|
||||
def delete_mapping(self, uuid):
|
||||
with db.session_for_write() as session:
|
||||
q = utils.model_query(
|
||||
models.HashMapMapping,
|
||||
session)
|
||||
q = q.filter(models.HashMapMapping.mapping_id == uuid)
|
||||
r = q.delete()
|
||||
if not r:
|
||||
raise api.NoSuchMapping(uuid)
|
||||
def delete_mapping(self, uuid, deleted_by=None):
|
||||
try:
|
||||
with db.session_for_write() as session:
|
||||
q = session.query(models.HashMapMapping)
|
||||
q = q.filter(
|
||||
models.HashMapMapping.mapping_id == uuid,
|
||||
models.HashMapMapping.deleted == sqlalchemy.null())
|
||||
mapping_db = q.with_for_update().one()
|
||||
mapping_db.deleted_by = deleted_by
|
||||
mapping_db.deleted = datetime.datetime.now()
|
||||
except sqlalchemy.orm.exc.NoResultFound:
|
||||
raise api.NoSuchMapping(uuid)
|
||||
|
||||
def delete_threshold(self, uuid):
|
||||
with db.session_for_write() as session:
|
||||
|
||||
@@ -20,6 +20,7 @@ from sqlalchemy import orm
|
||||
from sqlalchemy import schema
|
||||
|
||||
from cloudkitty.common.db import models as ck_models
|
||||
from cloudkitty.rating.common.db.models import VolatileAuditableModel
|
||||
|
||||
Base = ck_models.get_base()
|
||||
|
||||
@@ -190,7 +191,7 @@ class HashMapGroup(Base, HashMapBase):
|
||||
name=self.name)
|
||||
|
||||
|
||||
class HashMapMapping(Base, HashMapBase):
|
||||
class HashMapMapping(Base, HashMapBase, VolatileAuditableModel):
|
||||
"""A mapping between a field or service, a value and a type.
|
||||
|
||||
Used to model final equation.
|
||||
|
||||
@@ -33,7 +33,7 @@ class Noop(rating.RatingProcessorBase):
|
||||
def priority(self):
|
||||
return 1
|
||||
|
||||
def reload_config(self):
|
||||
def reload_config(self, start=None):
|
||||
pass
|
||||
|
||||
def process(self, data):
|
||||
|
||||
@@ -44,9 +44,9 @@ class PyScripts(rating.RatingProcessorBase):
|
||||
self.load_scripts_in_memory()
|
||||
super(PyScripts, self).__init__(tenant_id)
|
||||
|
||||
def load_scripts_in_memory(self):
|
||||
def load_scripts_in_memory(self, start=None):
|
||||
db = pyscripts_db_api.get_instance()
|
||||
scripts_uuid_list = db.list_scripts()
|
||||
scripts_uuid_list = db.list_scripts(is_active=start or True)
|
||||
self.purge_removed_scripts(scripts_uuid_list)
|
||||
|
||||
# Load or update script
|
||||
@@ -87,12 +87,12 @@ class PyScripts(rating.RatingProcessorBase):
|
||||
|
||||
del self._scripts[script_uuid]
|
||||
|
||||
def reload_config(self):
|
||||
def reload_config(self, start=None):
|
||||
"""Reload the module's configuration.
|
||||
|
||||
"""
|
||||
LOG.debug("Executing the reload of configurations.")
|
||||
self.load_scripts_in_memory()
|
||||
self.load_scripts_in_memory(start)
|
||||
LOG.debug("Configurations reloaded.")
|
||||
|
||||
def start_script(self, code, data):
|
||||
|
||||
@@ -13,12 +13,15 @@
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
#
|
||||
import datetime
|
||||
import pecan
|
||||
from wsme import types as wtypes
|
||||
import wsmeext.pecan as wsme_pecan
|
||||
|
||||
from cloudkitty.api.v1 import types as ck_types
|
||||
from cloudkitty.common.custom_session import get_request_user
|
||||
from cloudkitty import rating
|
||||
from cloudkitty.rating.common.validations import fields as field_validations
|
||||
from cloudkitty.rating.pyscripts.datamodels import script as script_models
|
||||
from cloudkitty.rating.pyscripts.db import api as db_api
|
||||
|
||||
@@ -39,18 +42,52 @@ class PyScriptsScriptsController(rating.RatingRestControllerBase):
|
||||
data = data.encode('utf-8')
|
||||
return data
|
||||
|
||||
@wsme_pecan.wsexpose(script_models.ScriptCollection, bool)
|
||||
def get_all(self, no_data=False):
|
||||
@wsme_pecan.wsexpose(script_models.ScriptCollection,
|
||||
bool,
|
||||
bool,
|
||||
datetime.datetime,
|
||||
datetime.datetime,
|
||||
str,
|
||||
str,
|
||||
str,
|
||||
str,
|
||||
bool)
|
||||
def get_all(self, no_data=False,
|
||||
deleted=False,
|
||||
start=None,
|
||||
end=None,
|
||||
updated_by=None,
|
||||
created_by=None,
|
||||
deleted_by=None,
|
||||
description=None,
|
||||
is_active=None):
|
||||
"""Get the script list
|
||||
|
||||
:param no_data: Set to True to remove script data from output.
|
||||
:param deleted: Show deleted mappings.
|
||||
:param start: Mappings with start after date.
|
||||
:param end: Mappings with end before date.
|
||||
:param updated_by: user uuid to filter on.
|
||||
:param created_by: user uuid to filter on.
|
||||
:param deleted_by: user uuid to filter on.
|
||||
:param description: mapping that contains the text in description.
|
||||
:param is_active: only active mappings.
|
||||
:return: List of every scripts.
|
||||
"""
|
||||
pyscripts = db_api.get_instance()
|
||||
script_list = []
|
||||
script_uuid_list = pyscripts.list_scripts()
|
||||
script_uuid_list = pyscripts.list_scripts(
|
||||
deleted=deleted,
|
||||
start=start,
|
||||
end=end,
|
||||
updated_by=updated_by,
|
||||
created_by=created_by,
|
||||
deleted_by=deleted_by,
|
||||
description=description,
|
||||
is_active=is_active)
|
||||
for script_uuid in script_uuid_list:
|
||||
script_db = pyscripts.get_script(uuid=script_uuid)
|
||||
script_db = pyscripts.get_script(uuid=script_uuid,
|
||||
deleted=deleted)
|
||||
script = script_db.export_model()
|
||||
if no_data:
|
||||
del script['data']
|
||||
@@ -73,17 +110,27 @@ class PyScriptsScriptsController(rating.RatingRestControllerBase):
|
||||
pecan.abort(404, e.args[0])
|
||||
|
||||
@wsme_pecan.wsexpose(script_models.Script,
|
||||
bool,
|
||||
body=script_models.Script,
|
||||
status_code=201)
|
||||
def post(self, script_data):
|
||||
def post(self, force=False, script_data=None):
|
||||
"""Create pyscripts script.
|
||||
|
||||
:param force: Allows start and end in the past.
|
||||
:param script_data: Informations about the script to create.
|
||||
"""
|
||||
pyscripts = db_api.get_instance()
|
||||
field_validations.validate_resource(
|
||||
script_data, force=force)
|
||||
try:
|
||||
created_by = get_request_user()
|
||||
data = self.normalize_data(script_data.data)
|
||||
script_db = pyscripts.create_script(script_data.name, data)
|
||||
script_db = pyscripts.create_script(
|
||||
script_data.name, data,
|
||||
created_by=created_by,
|
||||
start=script_data.start,
|
||||
end=script_data.end,
|
||||
description=script_data.description)
|
||||
pecan.response.location = pecan.request.path_url
|
||||
if pecan.response.location[-1] != '/':
|
||||
pecan.response.location += '/'
|
||||
@@ -105,10 +152,21 @@ class PyScriptsScriptsController(rating.RatingRestControllerBase):
|
||||
"""
|
||||
pyscripts = db_api.get_instance()
|
||||
try:
|
||||
updated_by = get_request_user()
|
||||
current_script = pyscripts.get_script(uuid=script_id)
|
||||
data = self.normalize_data(script_data.data)
|
||||
script_db = pyscripts.update_script(script_id,
|
||||
name=script_data.name,
|
||||
data=data)
|
||||
if field_validations.validate_update_allowing_only_end_date(
|
||||
current_script,
|
||||
script_data):
|
||||
script_db = pyscripts.update_script(
|
||||
script_id, end=script_data.end,
|
||||
updated_by=updated_by)
|
||||
else:
|
||||
script_db = pyscripts.update_script(
|
||||
script_id, data=data, updated_by=updated_by,
|
||||
end=script_data.end,
|
||||
description=script_data.description,
|
||||
start=script_data.start)
|
||||
pecan.response.location = pecan.request.path_url
|
||||
if pecan.response.location[-1] != '/':
|
||||
pecan.response.location += '/'
|
||||
@@ -125,7 +183,8 @@ class PyScriptsScriptsController(rating.RatingRestControllerBase):
|
||||
:param script_id: UUID of the script to delete.
|
||||
"""
|
||||
pyscripts = db_api.get_instance()
|
||||
deleted_by = get_request_user()
|
||||
try:
|
||||
pyscripts.delete_script(uuid=script_id)
|
||||
pyscripts.delete_script(uuid=script_id, deleted_by=deleted_by)
|
||||
except db_api.NoSuchScript as e:
|
||||
pecan.abort(404, e.args[0])
|
||||
|
||||
@@ -16,9 +16,10 @@
|
||||
from wsme import types as wtypes
|
||||
|
||||
from cloudkitty.api.v1 import types as ck_types
|
||||
from cloudkitty.rating.common.datamodels.models import VolatileAuditableModel
|
||||
|
||||
|
||||
class Script(wtypes.Base):
|
||||
class Script(VolatileAuditableModel):
|
||||
"""Type describing a script.
|
||||
|
||||
"""
|
||||
@@ -37,12 +38,14 @@ class Script(wtypes.Base):
|
||||
|
||||
@classmethod
|
||||
def sample(cls):
|
||||
sample = super().sample()
|
||||
sample = cls(script_id='bc05108d-f515-4984-8077-de319cbf35aa',
|
||||
name='policy1',
|
||||
data='return 0',
|
||||
checksum='cf83e1357eefb8bdf1542850d66d8007d620e4050b5715d'
|
||||
'c83f4a921d36ce9ce47d0d13c5d85f2b0ff8318d2877eec'
|
||||
'2f63b931bd47417a81a538327af927da3e')
|
||||
'2f63b931bd47417a81a538327af927da3e',
|
||||
**sample.__dict__)
|
||||
return sample
|
||||
|
||||
|
||||
|
||||
@@ -64,25 +64,34 @@ class PyScripts(object, metaclass=abc.ABCMeta):
|
||||
"""
|
||||
|
||||
@abc.abstractmethod
|
||||
def get_script(self, name=None, uuid=None):
|
||||
def get_script(self, name=None, uuid=None, deleted=None):
|
||||
"""Return a script object.
|
||||
|
||||
:param name: Filter on a script name.
|
||||
:param uuid: The uuid of the script to get.
|
||||
:param deleted: Show deleted script.
|
||||
"""
|
||||
|
||||
@abc.abstractmethod
|
||||
def list_scripts(self):
|
||||
def list_scripts(self, **kwargs):
|
||||
"""Return a UUID list of every scripts available.
|
||||
|
||||
"""
|
||||
|
||||
@abc.abstractmethod
|
||||
def create_script(self, name, data):
|
||||
def create_script(self, name, data,
|
||||
start=None,
|
||||
end=None,
|
||||
description=None,
|
||||
created_by=None):
|
||||
"""Create a new script.
|
||||
|
||||
:param name: Name of the script to create.
|
||||
:param data: Content of the python script.
|
||||
:param start: The date the script will start to be valid.
|
||||
:param end: The date the script will stop to be valid.
|
||||
:param description: The script description
|
||||
:param created_by: The user who created the script.
|
||||
"""
|
||||
|
||||
@abc.abstractmethod
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
#
|
||||
|
||||
"""Add start end dates and audit in pyscripts
|
||||
|
||||
Revision ID: c6e4cda29654
|
||||
Revises: 75c205f6f1a2
|
||||
Create Date: 2023-03-06 14:22:00.000000
|
||||
|
||||
"""
|
||||
|
||||
from alembic import op
|
||||
from cloudkitty import db
|
||||
from cloudkitty.rating.common.db.migrations import create_common_tables
|
||||
from cloudkitty.rating.pyscripts.db.sqlalchemy import models
|
||||
|
||||
import datetime
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = 'c6e4cda29654'
|
||||
down_revision = '75c205f6f1a2'
|
||||
|
||||
|
||||
def _update_start_date():
|
||||
# Year of the start of the project (not the first version)
|
||||
initial_start_date = datetime.datetime(year=2014, month=1, day=1)
|
||||
with db.session_for_write() as session:
|
||||
q = session.query(models.PyScriptsScript)
|
||||
mapping_db = q.with_for_update().all()
|
||||
for entry in mapping_db:
|
||||
entry.start = initial_start_date
|
||||
entry.created_by = 'migration'
|
||||
|
||||
|
||||
def upgrade():
|
||||
table_name = 'pyscripts_scripts'
|
||||
is_sqlite = op.get_context().dialect.name == 'sqlite'
|
||||
with op.batch_alter_table(
|
||||
table_name) as batch_op:
|
||||
if not is_sqlite:
|
||||
batch_op.drop_constraint(
|
||||
'name',
|
||||
type_='unique')
|
||||
create_common_tables(batch_op)
|
||||
|
||||
_update_start_date()
|
||||
@@ -13,12 +13,14 @@
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
#
|
||||
import datetime
|
||||
|
||||
from oslo_db import exception
|
||||
from oslo_db.sqlalchemy import utils
|
||||
from oslo_utils import uuidutils
|
||||
import sqlalchemy
|
||||
|
||||
from cloudkitty import db
|
||||
from cloudkitty.rating.common.db.filters import get_filters
|
||||
from cloudkitty.rating.pyscripts.db import api
|
||||
from cloudkitty.rating.pyscripts.db.sqlalchemy import migration
|
||||
from cloudkitty.rating.pyscripts.db.sqlalchemy import models
|
||||
@@ -33,7 +35,7 @@ class PyScripts(api.PyScripts):
|
||||
def get_migration(self):
|
||||
return migration
|
||||
|
||||
def get_script(self, name=None, uuid=None):
|
||||
def get_script(self, name=None, uuid=None, deleted=False):
|
||||
with db.session_for_read() as session:
|
||||
try:
|
||||
q = session.query(models.PyScriptsScript)
|
||||
@@ -45,22 +47,51 @@ class PyScripts(api.PyScripts):
|
||||
models.PyScriptsScript.script_id == uuid)
|
||||
else:
|
||||
raise ValueError('You must specify either name or uuid.')
|
||||
if not deleted:
|
||||
q = q.filter(
|
||||
models.PyScriptsScript.deleted == sqlalchemy.null())
|
||||
res = q.one()
|
||||
return res
|
||||
except sqlalchemy.orm.exc.NoResultFound:
|
||||
raise api.NoSuchScript(name=name, uuid=uuid)
|
||||
|
||||
def list_scripts(self):
|
||||
def list_scripts(self, **kwargs):
|
||||
with db.session_for_read() as session:
|
||||
q = session.query(models.PyScriptsScript)
|
||||
q = get_filters(q, models.PyScriptsScript, **kwargs)
|
||||
res = q.values(
|
||||
models.PyScriptsScript.script_id)
|
||||
return [uuid[0] for uuid in res]
|
||||
|
||||
def create_script(self, name, data):
|
||||
def create_script(self, name, data,
|
||||
start=None,
|
||||
end=None,
|
||||
description=None,
|
||||
created_by=None):
|
||||
created_at = datetime.datetime.now()
|
||||
try:
|
||||
with db.session_for_write() as session:
|
||||
script_db = models.PyScriptsScript(name=name)
|
||||
q = session.query(models.PyScriptsScript)
|
||||
q = q.filter(
|
||||
models.PyScriptsScript.name == name,
|
||||
models.PyScriptsScript.deleted == sqlalchemy.null()
|
||||
)
|
||||
script_db = q.with_for_update().all()
|
||||
if script_db:
|
||||
script_db = self.get_script(name=name)
|
||||
raise api.ScriptAlreadyExists(
|
||||
script_db.name,
|
||||
script_db.script_id)
|
||||
script_db = models.PyScriptsScript(
|
||||
name=name,
|
||||
start=start,
|
||||
created_at=created_at,
|
||||
end=end,
|
||||
description=description,
|
||||
deleted=None,
|
||||
created_by=created_by,
|
||||
updated_by=None,
|
||||
deleted_by=None)
|
||||
script_db.data = data
|
||||
script_db.script_id = uuidutils.generate_uuid()
|
||||
session.add(script_db)
|
||||
@@ -76,7 +107,8 @@ class PyScripts(api.PyScripts):
|
||||
with db.session_for_write() as session:
|
||||
q = session.query(models.PyScriptsScript)
|
||||
q = q.filter(
|
||||
models.PyScriptsScript.script_id == uuid
|
||||
models.PyScriptsScript.script_id == uuid,
|
||||
models.PyScriptsScript.deleted == sqlalchemy.null()
|
||||
)
|
||||
script_db = q.with_for_update().one()
|
||||
if kwargs:
|
||||
@@ -96,17 +128,22 @@ class PyScripts(api.PyScripts):
|
||||
except sqlalchemy.orm.exc.NoResultFound:
|
||||
raise api.NoSuchScript(uuid=uuid)
|
||||
|
||||
def delete_script(self, name=None, uuid=None):
|
||||
def delete_script(self, name=None, uuid=None, deleted_by=None):
|
||||
with db.session_for_write() as session:
|
||||
q = utils.model_query(
|
||||
models.PyScriptsScript,
|
||||
session)
|
||||
if name:
|
||||
q = q.filter(models.PyScriptsScript.name == name)
|
||||
elif uuid:
|
||||
q = q.filter(models.PyScriptsScript.script_id == uuid)
|
||||
else:
|
||||
raise ValueError('You must specify either name or uuid.')
|
||||
r = q.delete()
|
||||
if not r:
|
||||
try:
|
||||
q = session.query(models.PyScriptsScript)
|
||||
if name:
|
||||
q = q.filter(models.PyScriptsScript.name == name)
|
||||
elif uuid:
|
||||
q = q.filter(models.PyScriptsScript.script_id == uuid)
|
||||
else:
|
||||
raise ValueError(
|
||||
'You must specify either name or uuid.')
|
||||
q = q.filter(
|
||||
models.PyScriptsScript.deleted == sqlalchemy.null())
|
||||
|
||||
script_db = q.with_for_update().one()
|
||||
script_db.deleted_by = deleted_by
|
||||
script_db.deleted = datetime.datetime.now()
|
||||
except sqlalchemy.orm.exc.NoResultFound:
|
||||
raise api.NoSuchScript(uuid=uuid)
|
||||
|
||||
@@ -21,6 +21,8 @@ import sqlalchemy
|
||||
from sqlalchemy.ext import declarative
|
||||
from sqlalchemy.ext import hybrid
|
||||
|
||||
from cloudkitty.rating.common.db.models import VolatileAuditableModel
|
||||
|
||||
Base = declarative.declarative_base()
|
||||
|
||||
|
||||
@@ -59,7 +61,7 @@ class PyScriptsBase(models.ModelBase):
|
||||
return res
|
||||
|
||||
|
||||
class PyScriptsScript(Base, PyScriptsBase):
|
||||
class PyScriptsScript(Base, PyScriptsBase, VolatileAuditableModel):
|
||||
"""A PyScripts entry.
|
||||
|
||||
"""
|
||||
|
||||
@@ -55,7 +55,7 @@ class FakeRatingModule(rating.RatingProcessorBase):
|
||||
entry['rating'] = {'price': decimal.Decimal(0)}
|
||||
return data
|
||||
|
||||
def reload_config(self):
|
||||
def reload_config(self, start=None):
|
||||
pass
|
||||
|
||||
def notify_reload(self):
|
||||
|
||||
@@ -54,18 +54,26 @@ from cloudkitty.utils import tz as tzutils
|
||||
|
||||
|
||||
INITIAL_DT = datetime.datetime(2015, 1, 1, tzinfo=tz.tzutc())
|
||||
CURRENT = 0
|
||||
|
||||
|
||||
class UUIDFixture(fixture.GabbiFixture):
|
||||
|
||||
def incremental_uuid(self):
|
||||
global CURRENT
|
||||
CURRENT = CURRENT + 1
|
||||
return f'6c1b8a30-797f-4b7e-ad66-{str(CURRENT).zfill(12)}'
|
||||
|
||||
def start_fixture(self):
|
||||
FAKE_UUID = '6c1b8a30-797f-4b7e-ad66-9879b79059fb'
|
||||
patcher = mock.patch(
|
||||
'oslo_utils.uuidutils.generate_uuid',
|
||||
return_value=FAKE_UUID)
|
||||
side_effect=self.incremental_uuid)
|
||||
patcher.start()
|
||||
self.patcher = patcher
|
||||
|
||||
def stop_fixture(self):
|
||||
global CURRENT
|
||||
CURRENT = 0
|
||||
self.patcher.stop()
|
||||
|
||||
|
||||
|
||||
@@ -68,6 +68,8 @@ tests:
|
||||
data:
|
||||
service_id: "371bcd08-009f-11e6-91de-8745729038b2"
|
||||
type: "fail"
|
||||
start: "3000-01-01"
|
||||
name: 'teste3'
|
||||
cost: "0.2"
|
||||
status: 400
|
||||
response_strings:
|
||||
@@ -117,7 +119,7 @@ tests:
|
||||
- "No such mapping: 42"
|
||||
|
||||
- name: create a field mapping to check updates
|
||||
url: /v1/rating/module_config/hashmap/mappings
|
||||
url: /v1/rating/module_config/hashmap/mappings?force=true
|
||||
method: POST
|
||||
request_headers:
|
||||
content-type: application/json
|
||||
@@ -125,6 +127,9 @@ tests:
|
||||
data:
|
||||
field_id: $ENVIRON['hash_error_field_id']
|
||||
type: "flat"
|
||||
start: "2014-01-01"
|
||||
end: "2014-01-02"
|
||||
name: 'teste4'
|
||||
cost: "0.2"
|
||||
value: "fail"
|
||||
status: 201
|
||||
@@ -141,7 +146,7 @@ tests:
|
||||
value: ''
|
||||
status: 400
|
||||
response_strings:
|
||||
- "You must specify a value for a field mapping."
|
||||
- "Cannot update a rule that was already processed and has a defined end date."
|
||||
|
||||
- name: create a service mapping with an invalid service_id
|
||||
url: /v1/rating/module_config/hashmap/mappings
|
||||
@@ -152,6 +157,8 @@ tests:
|
||||
data:
|
||||
service_id: "de23e3fe-0097-11e6-a44d-2b09512e61d9"
|
||||
type: "flat"
|
||||
start: "3000-01-01"
|
||||
name: 'teste5'
|
||||
cost: "0.2"
|
||||
status: 400
|
||||
response_strings:
|
||||
@@ -167,6 +174,8 @@ tests:
|
||||
field_id: "de23e3fe-0097-11e6-a44d-2b09512e61d9"
|
||||
type: "flat"
|
||||
cost: "0.2"
|
||||
start: "3000-01-01"
|
||||
name: 'teste6'
|
||||
value: "fail"
|
||||
status: 400
|
||||
response_strings:
|
||||
@@ -212,6 +221,8 @@ tests:
|
||||
service_id: "de23e3fe-0097-11e6-a44d-2b09512e61d9"
|
||||
field_id: "de23e3fe-0097-11e6-a44d-2b09512e61d9"
|
||||
type: "flat"
|
||||
start: "3000-01-01"
|
||||
name: 'teste7'
|
||||
cost: "0.2"
|
||||
status: 400
|
||||
response_strings:
|
||||
@@ -225,6 +236,8 @@ tests:
|
||||
x-roles: admin
|
||||
data:
|
||||
type: "flat"
|
||||
start: "3000-01-01"
|
||||
name: 'teste8'
|
||||
cost: "0.2"
|
||||
value: "fail"
|
||||
status: 400
|
||||
@@ -240,6 +253,8 @@ tests:
|
||||
data:
|
||||
field_id: $ENVIRON['hash_error_field_id']
|
||||
type: "flat"
|
||||
start: "3000-01-01"
|
||||
name: 'teste9'
|
||||
cost: "0.2"
|
||||
status: 400
|
||||
response_strings:
|
||||
|
||||
@@ -14,10 +14,10 @@ tests:
|
||||
name: "cpu"
|
||||
status: 201
|
||||
response_json_paths:
|
||||
$.service_id: "6c1b8a30-797f-4b7e-ad66-9879b79059fb"
|
||||
$.service_id: "6c1b8a30-797f-4b7e-ad66-000000000001"
|
||||
$.name: "cpu"
|
||||
response_headers:
|
||||
location: $SCHEME://$NETLOC/v1/rating/module_config/hashmap/services/6c1b8a30-797f-4b7e-ad66-9879b79059fb
|
||||
location: $SCHEME://$NETLOC/v1/rating/module_config/hashmap/services/6c1b8a30-797f-4b7e-ad66-000000000001
|
||||
|
||||
- name: check redirect on service mapping creation
|
||||
url: /v1/rating/module_config/hashmap/mappings
|
||||
@@ -26,20 +26,22 @@ tests:
|
||||
content-type: application/json
|
||||
x-roles: admin
|
||||
data:
|
||||
service_id: "6c1b8a30-797f-4b7e-ad66-9879b79059fb"
|
||||
service_id: "6c1b8a30-797f-4b7e-ad66-000000000001"
|
||||
type: "flat"
|
||||
start: "3000-01-01"
|
||||
name: 'teste'
|
||||
cost: "0.1000000000000000055511151231"
|
||||
status: 201
|
||||
response_json_paths:
|
||||
$.mapping_id: "6c1b8a30-797f-4b7e-ad66-9879b79059fb"
|
||||
$.service_id: "6c1b8a30-797f-4b7e-ad66-9879b79059fb"
|
||||
$.mapping_id: "6c1b8a30-797f-4b7e-ad66-000000000002"
|
||||
$.service_id: "6c1b8a30-797f-4b7e-ad66-000000000001"
|
||||
$.type: "flat"
|
||||
$.cost: "0.1000000000000000055511151231"
|
||||
response_headers:
|
||||
location: '$SCHEME://$NETLOC/v1/rating/module_config/hashmap/mappings/6c1b8a30-797f-4b7e-ad66-9879b79059fb'
|
||||
location: '$SCHEME://$NETLOC/v1/rating/module_config/hashmap/mappings/6c1b8a30-797f-4b7e-ad66-000000000002'
|
||||
|
||||
- name: delete test mapping
|
||||
url: /v1/rating/module_config/hashmap/mappings/6c1b8a30-797f-4b7e-ad66-9879b79059fb
|
||||
url: /v1/rating/module_config/hashmap/mappings/6c1b8a30-797f-4b7e-ad66-000000000002
|
||||
method: DELETE
|
||||
status: 204
|
||||
|
||||
@@ -50,22 +52,22 @@ tests:
|
||||
content-type: application/json
|
||||
x-roles: admin
|
||||
data:
|
||||
service_id: "6c1b8a30-797f-4b7e-ad66-9879b79059fb"
|
||||
service_id: "6c1b8a30-797f-4b7e-ad66-000000000001"
|
||||
level: "2"
|
||||
type: "flat"
|
||||
cost: "0.10000000"
|
||||
status: 201
|
||||
response_json_paths:
|
||||
$.threshold_id: "6c1b8a30-797f-4b7e-ad66-9879b79059fb"
|
||||
$.service_id: "6c1b8a30-797f-4b7e-ad66-9879b79059fb"
|
||||
$.threshold_id: "6c1b8a30-797f-4b7e-ad66-000000000003"
|
||||
$.service_id: "6c1b8a30-797f-4b7e-ad66-000000000001"
|
||||
$.level: "2.00000000"
|
||||
$.type: "flat"
|
||||
$.cost: "0.1000000000000000055511151231"
|
||||
response_headers:
|
||||
location: '$SCHEME://$NETLOC/v1/rating/module_config/hashmap/thresholds/6c1b8a30-797f-4b7e-ad66-9879b79059fb'
|
||||
location: '$SCHEME://$NETLOC/v1/rating/module_config/hashmap/thresholds/6c1b8a30-797f-4b7e-ad66-000000000003'
|
||||
|
||||
- name: delete test threshold
|
||||
url: /v1/rating/module_config/hashmap/thresholds/6c1b8a30-797f-4b7e-ad66-9879b79059fb
|
||||
url: /v1/rating/module_config/hashmap/thresholds/6c1b8a30-797f-4b7e-ad66-000000000003
|
||||
method: DELETE
|
||||
status: 204
|
||||
|
||||
@@ -76,15 +78,15 @@ tests:
|
||||
content-type: application/json
|
||||
x-roles: admin
|
||||
data:
|
||||
service_id: "6c1b8a30-797f-4b7e-ad66-9879b79059fb"
|
||||
service_id: "6c1b8a30-797f-4b7e-ad66-000000000001"
|
||||
name: "flavor_id"
|
||||
status: 201
|
||||
response_json_paths:
|
||||
$.service_id: "6c1b8a30-797f-4b7e-ad66-9879b79059fb"
|
||||
$.service_id: "6c1b8a30-797f-4b7e-ad66-000000000001"
|
||||
$.name: "flavor_id"
|
||||
$.field_id: "6c1b8a30-797f-4b7e-ad66-9879b79059fb"
|
||||
$.field_id: "6c1b8a30-797f-4b7e-ad66-000000000004"
|
||||
response_headers:
|
||||
location: '$SCHEME://$NETLOC/v1/rating/module_config/hashmap/fields/6c1b8a30-797f-4b7e-ad66-9879b79059fb'
|
||||
location: '$SCHEME://$NETLOC/v1/rating/module_config/hashmap/fields/6c1b8a30-797f-4b7e-ad66-000000000004'
|
||||
|
||||
- name: check redirect on field mapping creation
|
||||
url: /v1/rating/module_config/hashmap/mappings
|
||||
@@ -93,19 +95,21 @@ tests:
|
||||
content-type: application/json
|
||||
x-roles: admin
|
||||
data:
|
||||
field_id: "6c1b8a30-797f-4b7e-ad66-9879b79059fb"
|
||||
field_id: "6c1b8a30-797f-4b7e-ad66-000000000004"
|
||||
value: "04774238-fcad-11e5-a90e-6391fd56aab2"
|
||||
type: "flat"
|
||||
start: "3000-01-01"
|
||||
name: 'teste2'
|
||||
cost: "0.10000000"
|
||||
status: 201
|
||||
response_json_paths:
|
||||
$.mapping_id: "6c1b8a30-797f-4b7e-ad66-9879b79059fb"
|
||||
$.field_id: "6c1b8a30-797f-4b7e-ad66-9879b79059fb"
|
||||
$.mapping_id: "6c1b8a30-797f-4b7e-ad66-000000000005"
|
||||
$.field_id: "6c1b8a30-797f-4b7e-ad66-000000000004"
|
||||
$.value: "04774238-fcad-11e5-a90e-6391fd56aab2"
|
||||
$.type: "flat"
|
||||
$.cost: "0.1000000000000000055511151231"
|
||||
response_headers:
|
||||
location: '$SCHEME://$NETLOC/v1/rating/module_config/hashmap/mappings/6c1b8a30-797f-4b7e-ad66-9879b79059fb'
|
||||
location: '$SCHEME://$NETLOC/v1/rating/module_config/hashmap/mappings/6c1b8a30-797f-4b7e-ad66-000000000005'
|
||||
|
||||
- name: check redirect on field threshold creation
|
||||
url: /v1/rating/module_config/hashmap/thresholds
|
||||
@@ -114,19 +118,19 @@ tests:
|
||||
content-type: application/json
|
||||
x-roles: admin
|
||||
data:
|
||||
field_id: "6c1b8a30-797f-4b7e-ad66-9879b79059fb"
|
||||
field_id: "6c1b8a30-797f-4b7e-ad66-000000000004"
|
||||
level: "2"
|
||||
type: "flat"
|
||||
cost: "0.10000000"
|
||||
status: 201
|
||||
response_json_paths:
|
||||
$.threshold_id: "6c1b8a30-797f-4b7e-ad66-9879b79059fb"
|
||||
$.field_id: "6c1b8a30-797f-4b7e-ad66-9879b79059fb"
|
||||
$.threshold_id: "6c1b8a30-797f-4b7e-ad66-000000000006"
|
||||
$.field_id: "6c1b8a30-797f-4b7e-ad66-000000000004"
|
||||
$.level: "2.00000000"
|
||||
$.type: "flat"
|
||||
$.cost: "0.1000000000000000055511151231"
|
||||
response_headers:
|
||||
location: '$SCHEME://$NETLOC/v1/rating/module_config/hashmap/thresholds/6c1b8a30-797f-4b7e-ad66-9879b79059fb'
|
||||
location: '$SCHEME://$NETLOC/v1/rating/module_config/hashmap/thresholds/6c1b8a30-797f-4b7e-ad66-000000000006'
|
||||
|
||||
- name: check redirect on group creation
|
||||
url: /v1/rating/module_config/hashmap/groups
|
||||
@@ -138,7 +142,7 @@ tests:
|
||||
name: "compute_uptime"
|
||||
status: 201
|
||||
response_json_paths:
|
||||
$.group_id: "6c1b8a30-797f-4b7e-ad66-9879b79059fb"
|
||||
$.group_id: "6c1b8a30-797f-4b7e-ad66-000000000007"
|
||||
$.name: "compute_uptime"
|
||||
response_headers:
|
||||
location: $SCHEME://$NETLOC/v1/rating/module_config/hashmap/groups/6c1b8a30-797f-4b7e-ad66-9879b79059fb
|
||||
location: $SCHEME://$NETLOC/v1/rating/module_config/hashmap/groups/6c1b8a30-797f-4b7e-ad66-000000000007
|
||||
|
||||
@@ -35,7 +35,7 @@ tests:
|
||||
$.name: "cpu"
|
||||
|
||||
- name: create a flat service mapping
|
||||
url: /v1/rating/module_config/hashmap/mappings
|
||||
url: /v1/rating/module_config/hashmap/mappings?force=true
|
||||
method: POST
|
||||
request_headers:
|
||||
content-type: application/json
|
||||
@@ -43,6 +43,8 @@ tests:
|
||||
data:
|
||||
service_id: $RESPONSE['$.service_id']
|
||||
type: "flat"
|
||||
start: "2014-01-01"
|
||||
name: 'teste10'
|
||||
cost: "0.10000000"
|
||||
status: 201
|
||||
response_json_paths:
|
||||
@@ -63,7 +65,7 @@ tests:
|
||||
$.services[0].name: "cpu"
|
||||
|
||||
- name: create a rate service mapping
|
||||
url: /v1/rating/module_config/hashmap/mappings
|
||||
url: /v1/rating/module_config/hashmap/mappings?force=true
|
||||
method: POST
|
||||
request_headers:
|
||||
content-type: application/json
|
||||
@@ -71,6 +73,8 @@ tests:
|
||||
data:
|
||||
service_id: $RESPONSE['$.services[0].service_id']
|
||||
type: "rate"
|
||||
start: "2014-01-01"
|
||||
name: 'teste11'
|
||||
cost: "0.2"
|
||||
status: 201
|
||||
response_json_paths:
|
||||
@@ -79,7 +83,7 @@ tests:
|
||||
$.cost: "0.2000000000000000111022302463"
|
||||
|
||||
- name: create a flat service mapping for a tenant
|
||||
url: /v1/rating/module_config/hashmap/mappings
|
||||
url: /v1/rating/module_config/hashmap/mappings?force=true
|
||||
method: POST
|
||||
request_headers:
|
||||
content-type: application/json
|
||||
@@ -87,6 +91,8 @@ tests:
|
||||
data:
|
||||
service_id: $ENVIRON['hash_service_id']
|
||||
type: "flat"
|
||||
start: "2014-01-01"
|
||||
name: 'teste12'
|
||||
cost: "0.2"
|
||||
tenant_id: "24a7fdae-27ff-11e6-8c4f-6b725a05bf50"
|
||||
status: 201
|
||||
@@ -177,7 +183,7 @@ tests:
|
||||
$.field_id: $RESPONSE['$.field_id']
|
||||
|
||||
- name: create a flat field mapping
|
||||
url: /v1/rating/module_config/hashmap/mappings
|
||||
url: /v1/rating/module_config/hashmap/mappings?force=true
|
||||
method: POST
|
||||
request_headers:
|
||||
content-type: application/json
|
||||
@@ -185,6 +191,8 @@ tests:
|
||||
data:
|
||||
field_id: $RESPONSE['$.field_id']
|
||||
type: "rate"
|
||||
start: "2014-01-01"
|
||||
name: 'teste13'
|
||||
cost: "0.2"
|
||||
value: "e2083e22-0004-11e6-82bd-2f02489b068b"
|
||||
status: 201
|
||||
@@ -208,7 +216,7 @@ tests:
|
||||
$.fields[0].name: "flavor_id"
|
||||
|
||||
- name: create a rate field mapping
|
||||
url: /v1/rating/module_config/hashmap/mappings
|
||||
url: /v1/rating/module_config/hashmap/mappings?force=true
|
||||
method: POST
|
||||
request_headers:
|
||||
content-type: application/json
|
||||
@@ -216,6 +224,8 @@ tests:
|
||||
data:
|
||||
field_id: $RESPONSE['$.fields[0].field_id']
|
||||
type: "rate"
|
||||
start: "2014-01-01"
|
||||
name: 'teste14'
|
||||
cost: "0.2"
|
||||
value: "f17a0674-0004-11e6-a16b-cf941f4668c4"
|
||||
status: 201
|
||||
@@ -236,7 +246,9 @@ tests:
|
||||
type: "rate"
|
||||
cost: "0.3"
|
||||
value: "f17a0674-0004-11e6-a16b-cf941f4668c4"
|
||||
status: 302
|
||||
status: 400
|
||||
response_json_paths:
|
||||
$.faultstring: "You are allowed to update only the attribute [end] as this rule is already running as it started on [2014-01-01 00:00:00]"
|
||||
|
||||
- name: check updated mapping
|
||||
url: /v1/rating/module_config/hashmap/mappings/$ENVIRON['hash_rate_mapping_id']
|
||||
@@ -245,7 +257,7 @@ tests:
|
||||
$.mapping_id: $ENVIRON['hash_rate_mapping_id']
|
||||
$.field_id: $ENVIRON['hash_field_id']
|
||||
$.type: "rate"
|
||||
$.cost: "0.2999999999999999888977697537"
|
||||
$.cost: "0.2000000000000000111022302463"
|
||||
$.value: "f17a0674-0004-11e6-a16b-cf941f4668c4"
|
||||
|
||||
- name: delete a field
|
||||
@@ -304,7 +316,7 @@ tests:
|
||||
hash_field_id: $.field_id
|
||||
|
||||
- name: create a field mapping for recursive delete
|
||||
url: /v1/rating/module_config/hashmap/mappings
|
||||
url: /v1/rating/module_config/hashmap/mappings?force=true
|
||||
method: POST
|
||||
request_headers:
|
||||
content-type: application/json
|
||||
@@ -312,6 +324,8 @@ tests:
|
||||
data:
|
||||
field_id: $RESPONSE['$.field_id']
|
||||
value: "flavor_id"
|
||||
start: "2014-01-01"
|
||||
name: 'teste15'
|
||||
cost: "0.1"
|
||||
status: 201
|
||||
response_store_environ:
|
||||
|
||||
@@ -35,12 +35,12 @@ tests:
|
||||
data: "a = 0"
|
||||
status: 201
|
||||
response_json_paths:
|
||||
$.script_id: "6c1b8a30-797f-4b7e-ad66-9879b79059fb"
|
||||
$.script_id: "6c1b8a30-797f-4b7e-ad66-000000000001"
|
||||
$.name: "policy1"
|
||||
$.data: "a = 0"
|
||||
$.checksum: "4c612e33c0e40b7bf53cf95fad47dbfbeab9dd62f9bc181a9d1c6f40a087782223c23f793e747b0466b9e6998c6ea54f4edbd20febd13edb13b55074b5ee1a5a"
|
||||
response_headers:
|
||||
location: '$SCHEME://$NETLOC/v1/rating/module_config/pyscripts/scripts/6c1b8a30-797f-4b7e-ad66-9879b79059fb'
|
||||
location: '$SCHEME://$NETLOC/v1/rating/module_config/pyscripts/scripts/6c1b8a30-797f-4b7e-ad66-000000000001'
|
||||
|
||||
- name: create duplicate policy script
|
||||
url: /v1/rating/module_config/pyscripts/scripts
|
||||
@@ -53,13 +53,13 @@ tests:
|
||||
data: "a = 0"
|
||||
status: 409
|
||||
response_strings:
|
||||
- "Script policy1 already exists (UUID: 6c1b8a30-797f-4b7e-ad66-9879b79059fb)"
|
||||
- "Script policy1 already exists (UUID: 6c1b8a30-797f-4b7e-ad66-000000000001)"
|
||||
|
||||
- name: list scripts
|
||||
url: /v1/rating/module_config/pyscripts/scripts
|
||||
status: 200
|
||||
response_json_paths:
|
||||
$.scripts[0].script_id: "6c1b8a30-797f-4b7e-ad66-9879b79059fb"
|
||||
$.scripts[0].script_id: "6c1b8a30-797f-4b7e-ad66-000000000001"
|
||||
$.scripts[0].name: "policy1"
|
||||
$.scripts[0].data: "a = 0"
|
||||
$.scripts[0].checksum: "4c612e33c0e40b7bf53cf95fad47dbfbeab9dd62f9bc181a9d1c6f40a087782223c23f793e747b0466b9e6998c6ea54f4edbd20febd13edb13b55074b5ee1a5a"
|
||||
@@ -68,21 +68,21 @@ tests:
|
||||
url: /v1/rating/module_config/pyscripts/scripts?no_data=true
|
||||
status: 200
|
||||
response_json_paths:
|
||||
$.scripts[0].script_id: "6c1b8a30-797f-4b7e-ad66-9879b79059fb"
|
||||
$.scripts[0].script_id: "6c1b8a30-797f-4b7e-ad66-000000000001"
|
||||
$.scripts[0].name: "policy1"
|
||||
$.scripts[0].checksum: "4c612e33c0e40b7bf53cf95fad47dbfbeab9dd62f9bc181a9d1c6f40a087782223c23f793e747b0466b9e6998c6ea54f4edbd20febd13edb13b55074b5ee1a5a"
|
||||
|
||||
- name: get script
|
||||
url: /v1/rating/module_config/pyscripts/scripts/6c1b8a30-797f-4b7e-ad66-9879b79059fb
|
||||
url: /v1/rating/module_config/pyscripts/scripts/6c1b8a30-797f-4b7e-ad66-000000000001
|
||||
status: 200
|
||||
response_json_paths:
|
||||
$.script_id: "6c1b8a30-797f-4b7e-ad66-9879b79059fb"
|
||||
$.script_id: "6c1b8a30-797f-4b7e-ad66-000000000001"
|
||||
$.name: "policy1"
|
||||
$.data: "a = 0"
|
||||
$.checksum: "4c612e33c0e40b7bf53cf95fad47dbfbeab9dd62f9bc181a9d1c6f40a087782223c23f793e747b0466b9e6998c6ea54f4edbd20febd13edb13b55074b5ee1a5a"
|
||||
|
||||
- name: modify script
|
||||
url: /v1/rating/module_config/pyscripts/scripts/6c1b8a30-797f-4b7e-ad66-9879b79059fb
|
||||
url: /v1/rating/module_config/pyscripts/scripts/6c1b8a30-797f-4b7e-ad66-000000000001
|
||||
method: PUT
|
||||
request_headers:
|
||||
content-type: application/json
|
||||
@@ -90,12 +90,9 @@ tests:
|
||||
data:
|
||||
name: "policy1"
|
||||
data: "a = 1"
|
||||
status: 201
|
||||
response_json_paths:
|
||||
$.script_id: "6c1b8a30-797f-4b7e-ad66-9879b79059fb"
|
||||
$.name: "policy1"
|
||||
$.data: "a = 1"
|
||||
$.checksum: "acb3095e24b13960484e75bce070e13e8a7728760517c31b34929a6f732841c652e9d2cc4d186bd02ef2e7495fab3c4850673bedc945cee7c74fea85eabd542c"
|
||||
status: 400
|
||||
response_strings:
|
||||
- "You are allowed to update only the attribute [end] as this rule is already running as it started on "
|
||||
|
||||
- name: modify unknown script
|
||||
url: /v1/rating/module_config/pyscripts/scripts/42
|
||||
@@ -111,19 +108,19 @@ tests:
|
||||
- "No such script: None (UUID: 42)"
|
||||
|
||||
- name: check updated script
|
||||
url: /v1/rating/module_config/pyscripts/scripts/6c1b8a30-797f-4b7e-ad66-9879b79059fb
|
||||
url: /v1/rating/module_config/pyscripts/scripts/6c1b8a30-797f-4b7e-ad66-000000000001
|
||||
request_headers:
|
||||
content-type: application/json
|
||||
x-roles: admin
|
||||
status: 200
|
||||
response_json_paths:
|
||||
$.script_id: "6c1b8a30-797f-4b7e-ad66-9879b79059fb"
|
||||
$.script_id: "6c1b8a30-797f-4b7e-ad66-000000000001"
|
||||
$.name: "policy1"
|
||||
$.data: "a = 1"
|
||||
$.checksum: "acb3095e24b13960484e75bce070e13e8a7728760517c31b34929a6f732841c652e9d2cc4d186bd02ef2e7495fab3c4850673bedc945cee7c74fea85eabd542c"
|
||||
$.data: "a = 0"
|
||||
$.checksum: "4c612e33c0e40b7bf53cf95fad47dbfbeab9dd62f9bc181a9d1c6f40a087782223c23f793e747b0466b9e6998c6ea54f4edbd20febd13edb13b55074b5ee1a5a"
|
||||
|
||||
- name: delete script
|
||||
url: /v1/rating/module_config/pyscripts/scripts/6c1b8a30-797f-4b7e-ad66-9879b79059fb
|
||||
url: /v1/rating/module_config/pyscripts/scripts/6c1b8a30-797f-4b7e-ad66-000000000001
|
||||
method: DELETE
|
||||
status: 204
|
||||
|
||||
|
||||
@@ -139,6 +139,8 @@ class HashMapRatingTest(tests.TestCase):
|
||||
'flavor')
|
||||
group_db = self._db_api.create_group('test_group')
|
||||
self._db_api.create_mapping(
|
||||
name=uuidutils.generate_uuid(False),
|
||||
created_by='1',
|
||||
value='m1.tiny',
|
||||
cost='1.337',
|
||||
map_type='flat',
|
||||
@@ -156,6 +158,8 @@ class HashMapRatingTest(tests.TestCase):
|
||||
'flavor')
|
||||
group_db = self._db_api.create_group('test_group')
|
||||
mapping_db = self._db_api.create_mapping(
|
||||
name=uuidutils.generate_uuid(False),
|
||||
created_by='1',
|
||||
value='m1.tiny',
|
||||
cost='1.337',
|
||||
map_type='flat',
|
||||
@@ -173,11 +177,15 @@ class HashMapRatingTest(tests.TestCase):
|
||||
service_db = self._db_api.create_service('compute')
|
||||
group_db = self._db_api.create_group('test_group')
|
||||
mapping_tiny = self._db_api.create_mapping(
|
||||
name=uuidutils.generate_uuid(False),
|
||||
created_by='1',
|
||||
cost='1.337',
|
||||
map_type='flat',
|
||||
service_id=service_db.service_id,
|
||||
group_id=group_db.group_id)
|
||||
self._db_api.create_mapping(
|
||||
name=uuidutils.generate_uuid(False),
|
||||
created_by='1',
|
||||
cost='42',
|
||||
map_type='flat',
|
||||
service_id=service_db.service_id)
|
||||
@@ -191,18 +199,24 @@ class HashMapRatingTest(tests.TestCase):
|
||||
'flavor')
|
||||
group_db = self._db_api.create_group('test_group')
|
||||
mapping_tiny = self._db_api.create_mapping(
|
||||
name=uuidutils.generate_uuid(False),
|
||||
created_by='1',
|
||||
value='m1.tiny',
|
||||
cost='1.337',
|
||||
map_type='flat',
|
||||
field_id=field_db.field_id,
|
||||
group_id=group_db.group_id)
|
||||
mapping_small = self._db_api.create_mapping(
|
||||
name=uuidutils.generate_uuid(False),
|
||||
created_by='1',
|
||||
value='m1.small',
|
||||
cost='3.1337',
|
||||
map_type='flat',
|
||||
field_id=field_db.field_id,
|
||||
group_id=group_db.group_id)
|
||||
self._db_api.create_mapping(
|
||||
name=uuidutils.generate_uuid(False),
|
||||
created_by='1',
|
||||
value='m1.large',
|
||||
cost='42',
|
||||
map_type='flat',
|
||||
@@ -219,18 +233,24 @@ class HashMapRatingTest(tests.TestCase):
|
||||
'flavor')
|
||||
group_db = self._db_api.create_group('test_group')
|
||||
self._db_api.create_mapping(
|
||||
name=uuidutils.generate_uuid(False),
|
||||
created_by='1',
|
||||
value='m1.tiny',
|
||||
cost='1.337',
|
||||
map_type='flat',
|
||||
field_id=field_db.field_id,
|
||||
group_id=group_db.group_id)
|
||||
self._db_api.create_mapping(
|
||||
name=uuidutils.generate_uuid(False),
|
||||
created_by='1',
|
||||
value='m1.small',
|
||||
cost='3.1337',
|
||||
map_type='flat',
|
||||
field_id=field_db.field_id,
|
||||
group_id=group_db.group_id)
|
||||
mapping_no_group = self._db_api.create_mapping(
|
||||
name=uuidutils.generate_uuid(False),
|
||||
created_by='1',
|
||||
value='m1.large',
|
||||
cost='42',
|
||||
map_type='flat',
|
||||
@@ -324,6 +344,8 @@ class HashMapRatingTest(tests.TestCase):
|
||||
field_db = self._db_api.create_field(service_db.service_id,
|
||||
'flavor')
|
||||
mapping_db = self._db_api.create_mapping(
|
||||
name=uuidutils.generate_uuid(False),
|
||||
created_by='1',
|
||||
value='m1.tiny',
|
||||
cost='1.337',
|
||||
map_type='flat',
|
||||
@@ -336,6 +358,8 @@ class HashMapRatingTest(tests.TestCase):
|
||||
field_db = self._db_api.create_field(service_db.service_id,
|
||||
'flavor')
|
||||
mapping_db = self._db_api.create_mapping(
|
||||
name=uuidutils.generate_uuid(False),
|
||||
created_by='1',
|
||||
value='m1.tiny',
|
||||
cost='1.337',
|
||||
map_type='flat',
|
||||
@@ -350,6 +374,8 @@ class HashMapRatingTest(tests.TestCase):
|
||||
def test_list_mappings_from_services(self):
|
||||
service_db = self._db_api.create_service('compute')
|
||||
mapping_db = self._db_api.create_mapping(
|
||||
name=uuidutils.generate_uuid(False),
|
||||
created_by='1',
|
||||
cost='1.337',
|
||||
map_type='flat',
|
||||
service_id=service_db.service_id)
|
||||
@@ -362,6 +388,8 @@ class HashMapRatingTest(tests.TestCase):
|
||||
field_db = self._db_api.create_field(service_db.service_id,
|
||||
'flavor')
|
||||
mapping_db = self._db_api.create_mapping(
|
||||
name=uuidutils.generate_uuid(False),
|
||||
created_by='1',
|
||||
value='m1.tiny',
|
||||
cost='1.337',
|
||||
map_type='flat',
|
||||
@@ -398,6 +426,8 @@ class HashMapRatingTest(tests.TestCase):
|
||||
field_db = self._db_api.create_field(service_db.service_id,
|
||||
'flavor')
|
||||
mapping_db = self._db_api.create_mapping(
|
||||
name=uuidutils.generate_uuid(False),
|
||||
created_by='1',
|
||||
value='m1.tiny',
|
||||
cost='1.337',
|
||||
map_type='flat',
|
||||
@@ -414,6 +444,8 @@ class HashMapRatingTest(tests.TestCase):
|
||||
field_db = self._db_api.create_field(service_db.service_id,
|
||||
'flavor')
|
||||
mapping_db = self._db_api.create_mapping(
|
||||
name=uuidutils.generate_uuid(False),
|
||||
created_by='1',
|
||||
value='m1.tiny',
|
||||
cost='1.337',
|
||||
map_type='flat',
|
||||
@@ -433,6 +465,8 @@ class HashMapRatingTest(tests.TestCase):
|
||||
field_db = self._db_api.create_field(service_db.service_id,
|
||||
'flavor')
|
||||
mapping_db = self._db_api.create_mapping(
|
||||
name=uuidutils.generate_uuid(False),
|
||||
created_by='1',
|
||||
value='m1.tiny',
|
||||
cost='1.337',
|
||||
map_type='flat',
|
||||
@@ -447,6 +481,8 @@ class HashMapRatingTest(tests.TestCase):
|
||||
service_db.service_id,
|
||||
'flavor')
|
||||
mapping_db = self._db_api.create_mapping(
|
||||
name=uuidutils.generate_uuid(False),
|
||||
created_by='1',
|
||||
value='m1.tiny',
|
||||
cost='1.337',
|
||||
map_type='flat',
|
||||
@@ -464,12 +500,16 @@ class HashMapRatingTest(tests.TestCase):
|
||||
service_db.service_id,
|
||||
'flavor')
|
||||
mapping_db = self._db_api.create_mapping(
|
||||
name=uuidutils.generate_uuid(False),
|
||||
created_by='1',
|
||||
value='m1.tiny',
|
||||
cost='1.337',
|
||||
map_type='flat',
|
||||
field_id=field_db.field_id,
|
||||
tenant_id=self._tenant_id)
|
||||
self._db_api.create_mapping(
|
||||
name=uuidutils.generate_uuid(False),
|
||||
created_by='1',
|
||||
value='m1.small',
|
||||
cost='1.337',
|
||||
map_type='flat',
|
||||
@@ -485,11 +525,15 @@ class HashMapRatingTest(tests.TestCase):
|
||||
service_db.service_id,
|
||||
'flavor')
|
||||
mapping_db = self._db_api.create_mapping(
|
||||
name=uuidutils.generate_uuid(False),
|
||||
created_by='1',
|
||||
value='m1.tiny',
|
||||
cost='1.337',
|
||||
map_type='flat',
|
||||
field_id=field_db.field_id)
|
||||
self._db_api.create_mapping(
|
||||
name=uuidutils.generate_uuid(False),
|
||||
created_by='1',
|
||||
value='m1.small',
|
||||
cost='1.337',
|
||||
map_type='flat',
|
||||
@@ -711,17 +755,23 @@ class HashMapRatingTest(tests.TestCase):
|
||||
group_db = self._db_api.create_group('test_group')
|
||||
mapping_list.append(
|
||||
self._db_api.create_mapping(
|
||||
name=uuidutils.generate_uuid(False),
|
||||
created_by='1',
|
||||
cost='1.42',
|
||||
map_type='rate',
|
||||
service_id=service_db.service_id))
|
||||
mapping_list.append(
|
||||
self._db_api.create_mapping(
|
||||
name=uuidutils.generate_uuid(False),
|
||||
created_by='1',
|
||||
value='m1.tiny',
|
||||
cost='1.337',
|
||||
map_type='flat',
|
||||
field_id=flavor_field.field_id))
|
||||
mapping_list.append(
|
||||
self._db_api.create_mapping(
|
||||
name=uuidutils.generate_uuid(False),
|
||||
created_by='1',
|
||||
value='m1.large',
|
||||
cost='13.37',
|
||||
map_type='rate',
|
||||
@@ -730,6 +780,8 @@ class HashMapRatingTest(tests.TestCase):
|
||||
# Per tenant override
|
||||
mapping_list.append(
|
||||
self._db_api.create_mapping(
|
||||
name=uuidutils.generate_uuid(False),
|
||||
created_by='1',
|
||||
value='m1.tiny',
|
||||
cost='2',
|
||||
map_type='flat',
|
||||
@@ -808,12 +860,16 @@ class HashMapRatingTest(tests.TestCase):
|
||||
group_db = self._db_api.create_group('test_group')
|
||||
mapping_list.append(
|
||||
self._db_api.create_mapping(
|
||||
name=uuidutils.generate_uuid(False),
|
||||
created_by='1',
|
||||
value='m1.tiny',
|
||||
cost='1.337',
|
||||
map_type='flat',
|
||||
field_id=field_db.field_id))
|
||||
mapping_list.append(
|
||||
self._db_api.create_mapping(
|
||||
name=uuidutils.generate_uuid(False),
|
||||
created_by='1',
|
||||
value='m1.large',
|
||||
cost='13.37',
|
||||
map_type='rate',
|
||||
@@ -859,11 +915,17 @@ class HashMapRatingTest(tests.TestCase):
|
||||
service_db = self._db_api.create_service('compute')
|
||||
group_db = self._db_api.create_group('test_group')
|
||||
self._db_api.create_mapping(
|
||||
name=uuidutils.generate_uuid(False),
|
||||
created_by='1',
|
||||
cost='1.337',
|
||||
start=datetime.datetime(2014, 1, 1),
|
||||
map_type='flat',
|
||||
service_id=service_db.service_id,
|
||||
group_id=group_db.group_id)
|
||||
self._db_api.create_mapping(
|
||||
name=uuidutils.generate_uuid(False),
|
||||
created_by='1',
|
||||
start=datetime.datetime(2014, 1, 3),
|
||||
cost='1.42',
|
||||
map_type='flat',
|
||||
service_id=service_db.service_id)
|
||||
@@ -899,18 +961,24 @@ class HashMapRatingTest(tests.TestCase):
|
||||
'image_id')
|
||||
group_db = self._db_api.create_group('test_group')
|
||||
self._db_api.create_mapping(
|
||||
name=uuidutils.generate_uuid(False),
|
||||
created_by='1',
|
||||
value='m1.nano',
|
||||
cost='1.337',
|
||||
map_type='flat',
|
||||
field_id=flavor_field.field_id,
|
||||
group_id=group_db.group_id)
|
||||
self._db_api.create_mapping(
|
||||
name=uuidutils.generate_uuid(False),
|
||||
created_by='1',
|
||||
value='a41fba37-2429-4f15-aa00-b5bc4bf557bf',
|
||||
cost='1.10',
|
||||
map_type='rate',
|
||||
field_id=image_field.field_id,
|
||||
group_id=group_db.group_id)
|
||||
self._db_api.create_mapping(
|
||||
name=uuidutils.generate_uuid(False),
|
||||
created_by='1',
|
||||
value='m1.tiny',
|
||||
cost='1.42',
|
||||
map_type='flat',
|
||||
@@ -943,6 +1011,8 @@ class HashMapRatingTest(tests.TestCase):
|
||||
flavor_field = self._db_api.create_field(service_db.service_id,
|
||||
'flavor')
|
||||
self._db_api.create_mapping(
|
||||
name=uuidutils.generate_uuid(False),
|
||||
created_by='1',
|
||||
value='non-existent',
|
||||
cost='1.337',
|
||||
map_type='flat',
|
||||
@@ -1148,22 +1218,30 @@ class HashMapRatingTest(tests.TestCase):
|
||||
group_db = self._db_api.create_group('test_group')
|
||||
second_group_db = self._db_api.create_group('second_test_group')
|
||||
self._db_api.create_mapping(
|
||||
name=uuidutils.generate_uuid(False),
|
||||
created_by='1',
|
||||
cost='1.00',
|
||||
map_type='flat',
|
||||
service_id=service_db.service_id)
|
||||
self._db_api.create_mapping(
|
||||
name=uuidutils.generate_uuid(False),
|
||||
created_by='1',
|
||||
value='m1.nano',
|
||||
cost='1.337',
|
||||
map_type='flat',
|
||||
field_id=flavor_db.field_id,
|
||||
group_id=group_db.group_id)
|
||||
self._db_api.create_mapping(
|
||||
name=uuidutils.generate_uuid(False),
|
||||
created_by='1',
|
||||
value='m1.tiny',
|
||||
cost='1.42',
|
||||
map_type='flat',
|
||||
field_id=flavor_db.field_id,
|
||||
group_id=group_db.group_id)
|
||||
self._db_api.create_mapping(
|
||||
name=uuidutils.generate_uuid(False),
|
||||
created_by='1',
|
||||
value='8',
|
||||
cost='16.0',
|
||||
map_type='flat',
|
||||
@@ -1172,6 +1250,8 @@ class HashMapRatingTest(tests.TestCase):
|
||||
image_db = self._db_api.create_field(service_db.service_id,
|
||||
'image_id')
|
||||
self._db_api.create_mapping(
|
||||
name=uuidutils.generate_uuid(False),
|
||||
created_by='1',
|
||||
value='a41fba37-2429-4f15-aa00-b5bc4bf557bf',
|
||||
cost='1.10',
|
||||
map_type='rate',
|
||||
|
||||
@@ -236,10 +236,17 @@ class WorkerTest(tests.TestCase):
|
||||
def setUp(self):
|
||||
super(WorkerTest, self).setUp()
|
||||
|
||||
patcher_state_manager_get_state = mock.patch(
|
||||
"cloudkitty.storage_state.StateManager"
|
||||
".get_last_processed_timestamp")
|
||||
patcher_state_manager_set_state = mock.patch(
|
||||
"cloudkitty.storage_state."
|
||||
"StateManager.set_last_processed_timestamp")
|
||||
self.addCleanup(patcher_state_manager_set_state.stop)
|
||||
self.state_manager_get_state_mock = \
|
||||
patcher_state_manager_get_state.start()
|
||||
self.state_manager_get_state_mock.return_value = datetime.datetime(
|
||||
2019, 7, 16, 8, 55, 1)
|
||||
self.state_manager_set_state_mock = \
|
||||
patcher_state_manager_set_state.start()
|
||||
|
||||
@@ -882,6 +889,13 @@ class ReprocessingWorkerTest(tests.TestCase):
|
||||
def setUp(self):
|
||||
super(ReprocessingWorkerTest, self).setUp()
|
||||
|
||||
patcher_state_manager_get_state = mock.patch(
|
||||
"cloudkitty.storage_state.StateManager"
|
||||
".get_last_processed_timestamp")
|
||||
self.state_manager_get_state_mock = \
|
||||
patcher_state_manager_get_state.start()
|
||||
self.state_manager_get_state_mock.return_value = datetime.datetime(
|
||||
2019, 7, 16, 8, 55, 1)
|
||||
patcher_reprocessing_scheduler_db_get_from_db = mock.patch(
|
||||
"cloudkitty.storage_state.ReprocessingSchedulerDb.get_from_db")
|
||||
self.addCleanup(patcher_reprocessing_scheduler_db_get_from_db.stop)
|
||||
|
||||
@@ -214,48 +214,56 @@ class PyScriptsRatingTest(tests.TestCase):
|
||||
@mock.patch.object(uuidutils, 'generate_uuid',
|
||||
return_value=FAKE_UUID)
|
||||
def test_create_script(self, patch_generate_uuid):
|
||||
self._db_api.create_script('policy1', TEST_CODE1)
|
||||
self._db_api.create_script('policy1', TEST_CODE1,
|
||||
created_by='')
|
||||
scripts = self._db_api.list_scripts()
|
||||
self.assertEqual([FAKE_UUID], scripts)
|
||||
patch_generate_uuid.assert_called_once_with()
|
||||
|
||||
def test_create_duplicate_script(self):
|
||||
self._db_api.create_script('policy1', TEST_CODE1)
|
||||
self._db_api.create_script('policy1', TEST_CODE1,
|
||||
created_by='')
|
||||
self.assertRaises(api.ScriptAlreadyExists,
|
||||
self._db_api.create_script,
|
||||
'policy1',
|
||||
TEST_CODE1)
|
||||
TEST_CODE1, created_by='')
|
||||
|
||||
def test_get_script_by_uuid(self):
|
||||
expected = self._db_api.create_script('policy1', TEST_CODE1)
|
||||
expected = self._db_api.create_script('policy1', TEST_CODE1,
|
||||
created_by='')
|
||||
actual = self._db_api.get_script(uuid=expected.script_id)
|
||||
self.assertEqual(expected.data, actual.data)
|
||||
|
||||
def test_get_script_by_name(self):
|
||||
expected = self._db_api.create_script('policy1', TEST_CODE1)
|
||||
expected = self._db_api.create_script('policy1', TEST_CODE1,
|
||||
created_by='')
|
||||
actual = self._db_api.get_script(expected.name)
|
||||
self.assertEqual(expected.data, actual.data)
|
||||
|
||||
def test_get_script_without_parameters(self):
|
||||
self._db_api.create_script('policy1', TEST_CODE1)
|
||||
self._db_api.create_script('policy1', TEST_CODE1,
|
||||
created_by='')
|
||||
self.assertRaises(
|
||||
ValueError,
|
||||
self._db_api.get_script)
|
||||
|
||||
def test_delete_script_by_name(self):
|
||||
self._db_api.create_script('policy1', TEST_CODE1)
|
||||
self._db_api.create_script('policy1', TEST_CODE1,
|
||||
created_by='')
|
||||
self._db_api.delete_script('policy1')
|
||||
scripts = self._db_api.list_scripts()
|
||||
self.assertEqual([], scripts)
|
||||
|
||||
def test_delete_script_by_uuid(self):
|
||||
script_db = self._db_api.create_script('policy1', TEST_CODE1)
|
||||
script_db = self._db_api.create_script('policy1', TEST_CODE1,
|
||||
created_by='')
|
||||
self._db_api.delete_script(uuid=script_db.script_id)
|
||||
scripts = self._db_api.list_scripts()
|
||||
self.assertEqual([], scripts)
|
||||
|
||||
def test_delete_script_without_parameters(self):
|
||||
self._db_api.create_script('policy1', TEST_CODE1)
|
||||
self._db_api.create_script('policy1', TEST_CODE1,
|
||||
created_by='')
|
||||
self.assertRaises(
|
||||
ValueError,
|
||||
self._db_api.delete_script)
|
||||
@@ -272,13 +280,15 @@ class PyScriptsRatingTest(tests.TestCase):
|
||||
uuid='6e8de9fc-ee17-4b60-b81a-c9320e994e76')
|
||||
|
||||
def test_update_script(self):
|
||||
script_db = self._db_api.create_script('policy1', TEST_CODE1)
|
||||
script_db = self._db_api.create_script('policy1', TEST_CODE1,
|
||||
created_by='')
|
||||
self._db_api.update_script(script_db.script_id, data=TEST_CODE2)
|
||||
actual = self._db_api.get_script(uuid=script_db.script_id)
|
||||
self.assertEqual(TEST_CODE2, actual.data)
|
||||
|
||||
def test_update_script_uuid_disabled(self):
|
||||
expected = self._db_api.create_script('policy1', TEST_CODE1)
|
||||
expected = self._db_api.create_script('policy1', TEST_CODE1,
|
||||
created_by='')
|
||||
self._db_api.update_script(expected.script_id,
|
||||
data=TEST_CODE2,
|
||||
script_id='42')
|
||||
@@ -286,7 +296,8 @@ class PyScriptsRatingTest(tests.TestCase):
|
||||
self.assertEqual(expected.script_id, actual.script_id)
|
||||
|
||||
def test_update_script_unknown_attribute(self):
|
||||
expected = self._db_api.create_script('policy1', TEST_CODE1)
|
||||
expected = self._db_api.create_script('policy1', TEST_CODE1,
|
||||
created_by='')
|
||||
self.assertRaises(
|
||||
ValueError,
|
||||
self._db_api.update_script,
|
||||
@@ -294,7 +305,8 @@ class PyScriptsRatingTest(tests.TestCase):
|
||||
nonexistent=1)
|
||||
|
||||
def test_empty_script_update(self):
|
||||
expected = self._db_api.create_script('policy1', TEST_CODE1)
|
||||
expected = self._db_api.create_script('policy1', TEST_CODE1,
|
||||
created_by='')
|
||||
self.assertRaises(
|
||||
ValueError,
|
||||
self._db_api.update_script,
|
||||
@@ -303,19 +315,22 @@ class PyScriptsRatingTest(tests.TestCase):
|
||||
# Storage tests
|
||||
def test_compressed_data(self):
|
||||
data = TEST_CODE1
|
||||
self._db_api.create_script('policy1', data)
|
||||
self._db_api.create_script('policy1', data,
|
||||
created_by='')
|
||||
script = self._db_api.get_script('policy1')
|
||||
expected = zlib.compress(data)
|
||||
self.assertEqual(expected, script._data)
|
||||
|
||||
def test_on_the_fly_decompression(self):
|
||||
data = TEST_CODE1
|
||||
self._db_api.create_script('policy1', data)
|
||||
self._db_api.create_script('policy1', data,
|
||||
created_by='')
|
||||
script = self._db_api.get_script('policy1')
|
||||
self.assertEqual(data, script.data)
|
||||
|
||||
def test_script_repr(self):
|
||||
script_db = self._db_api.create_script('policy1', TEST_CODE1)
|
||||
script_db = self._db_api.create_script('policy1', TEST_CODE1,
|
||||
created_by='')
|
||||
self.assertEqual(
|
||||
'<PyScripts Script[{uuid}]: name={name}>'.format(
|
||||
uuid=script_db.script_id,
|
||||
@@ -324,12 +339,14 @@ class PyScriptsRatingTest(tests.TestCase):
|
||||
|
||||
# Checksum tests
|
||||
def test_validate_checksum(self):
|
||||
self._db_api.create_script('policy1', TEST_CODE1)
|
||||
self._db_api.create_script('policy1', TEST_CODE1,
|
||||
created_by='')
|
||||
script = self._db_api.get_script('policy1')
|
||||
self.assertEqual(TEST_CODE1_CHECKSUM, script.checksum)
|
||||
|
||||
def test_read_only_checksum(self):
|
||||
self._db_api.create_script('policy1', TEST_CODE1)
|
||||
self._db_api.create_script('policy1', TEST_CODE1,
|
||||
created_by='')
|
||||
script = self._db_api.get_script('policy1')
|
||||
self.assertRaises(
|
||||
AttributeError,
|
||||
@@ -340,22 +357,27 @@ class PyScriptsRatingTest(tests.TestCase):
|
||||
'7d0d13c5d85f2b0ff8318d2877eec2f63b931bd47417a81a538327af927da3e')
|
||||
|
||||
def test_update_checksum(self):
|
||||
self._db_api.create_script('policy1', TEST_CODE1)
|
||||
self._db_api.create_script('policy1', TEST_CODE1,
|
||||
created_by='')
|
||||
script = self._db_api.get_script('policy1')
|
||||
script = self._db_api.update_script(script.script_id, data=TEST_CODE2)
|
||||
self.assertEqual(TEST_CODE2_CHECKSUM, script.checksum)
|
||||
|
||||
# Code exec tests
|
||||
def test_load_scripts(self):
|
||||
policy1_db = self._db_api.create_script('policy1', TEST_CODE1)
|
||||
policy2_db = self._db_api.create_script('policy2', TEST_CODE2)
|
||||
policy1_db = self._db_api.create_script('policy1', TEST_CODE1,
|
||||
created_by='')
|
||||
policy2_db = self._db_api.create_script('policy2', TEST_CODE2,
|
||||
created_by='')
|
||||
self._pyscripts.load_scripts_in_memory()
|
||||
self.assertIn(policy1_db.script_id, self._pyscripts._scripts)
|
||||
self.assertIn(policy2_db.script_id, self._pyscripts._scripts)
|
||||
|
||||
def test_purge_old_scripts(self):
|
||||
policy1_db = self._db_api.create_script('policy1', TEST_CODE1)
|
||||
policy2_db = self._db_api.create_script('policy2', TEST_CODE2)
|
||||
policy1_db = self._db_api.create_script('policy1', TEST_CODE1,
|
||||
created_by='')
|
||||
policy2_db = self._db_api.create_script('policy2', TEST_CODE2,
|
||||
created_by='')
|
||||
self._pyscripts.reload_config()
|
||||
self.assertIn(policy1_db.script_id, self._pyscripts._scripts)
|
||||
self.assertIn(policy2_db.script_id, self._pyscripts._scripts)
|
||||
@@ -367,7 +389,8 @@ class PyScriptsRatingTest(tests.TestCase):
|
||||
@mock.patch.object(uuidutils, 'generate_uuid',
|
||||
return_value=FAKE_UUID)
|
||||
def test_valid_script_data_loaded(self, patch_generate_uuid):
|
||||
self._db_api.create_script('policy1', TEST_CODE1)
|
||||
self._db_api.create_script('policy1', TEST_CODE1,
|
||||
created_by='')
|
||||
self._pyscripts.load_scripts_in_memory()
|
||||
expected = {
|
||||
FAKE_UUID: {
|
||||
@@ -384,7 +407,8 @@ class PyScriptsRatingTest(tests.TestCase):
|
||||
self.assertEqual(1, context['a'])
|
||||
|
||||
def test_update_script_on_checksum_change(self):
|
||||
policy_db = self._db_api.create_script('policy1', TEST_CODE1)
|
||||
policy_db = self._db_api.create_script('policy1', TEST_CODE1,
|
||||
created_by='')
|
||||
self._pyscripts.reload_config()
|
||||
self._db_api.update_script(policy_db.script_id, data=TEST_CODE2)
|
||||
self._pyscripts.reload_config()
|
||||
@@ -393,8 +417,10 @@ class PyScriptsRatingTest(tests.TestCase):
|
||||
self._pyscripts._scripts[policy_db.script_id]['checksum'])
|
||||
|
||||
def test_exec_code_isolation(self):
|
||||
self._db_api.create_script('policy1', TEST_CODE1)
|
||||
self._db_api.create_script('policy2', TEST_CODE3)
|
||||
self._db_api.create_script('policy1', TEST_CODE1,
|
||||
created_by='')
|
||||
self._db_api.create_script('policy2', TEST_CODE3,
|
||||
created_by='')
|
||||
self._pyscripts.reload_config()
|
||||
|
||||
self.assertEqual(2, len(self._pyscripts._scripts))
|
||||
@@ -403,7 +429,8 @@ class PyScriptsRatingTest(tests.TestCase):
|
||||
|
||||
# Processing
|
||||
def test_process_rating(self):
|
||||
self._db_api.create_script('policy1', COMPLEX_POLICY1)
|
||||
self._db_api.create_script('policy1', COMPLEX_POLICY1,
|
||||
created_by='')
|
||||
self._pyscripts.reload_config()
|
||||
|
||||
data_output = self._pyscripts.process(self.dataframe_for_tests)
|
||||
@@ -413,21 +440,22 @@ class PyScriptsRatingTest(tests.TestCase):
|
||||
for point in dict_output['usage']['compute']:
|
||||
if point['groupby'].get('flavor') == 'm1.nano':
|
||||
self.assertEqual(
|
||||
decimal.Decimal('2'), point['rating']['price'])
|
||||
decimal.Decimal('2'), point['rating']['price'])
|
||||
else:
|
||||
self.assertEqual(
|
||||
decimal.Decimal('0'), point['rating']['price'])
|
||||
for point in dict_output['usage']['instance_status']:
|
||||
if point['groupby'].get('flavor') == 'm1.ultra':
|
||||
self.assertEqual(
|
||||
decimal.Decimal('96'), point['rating']['price'])
|
||||
decimal.Decimal('96'), point['rating']['price'])
|
||||
else:
|
||||
self.assertEqual(
|
||||
decimal.Decimal('0'), point['rating']['price'])
|
||||
|
||||
# Processing
|
||||
def test_process_rating_with_documentation_rules(self):
|
||||
self._db_api.create_script('policy1', DOCUMENTATION_RATING_POLICY)
|
||||
self._db_api.create_script('policy1', DOCUMENTATION_RATING_POLICY,
|
||||
created_by='')
|
||||
self._pyscripts.reload_config()
|
||||
|
||||
dataframe_for_tests = copy.deepcopy(self.dataframe_for_tests)
|
||||
@@ -456,7 +484,7 @@ class PyScriptsRatingTest(tests.TestCase):
|
||||
for point in dict_output['usage']['instance_status']:
|
||||
if point['groupby'].get('flavor') == 'm1.ultra':
|
||||
self.assertEqual(
|
||||
decimal.Decimal('0'), point['rating']['price'])
|
||||
decimal.Decimal('0'), point['rating']['price'])
|
||||
else:
|
||||
self.assertEqual(
|
||||
decimal.Decimal('0'), point['rating']['price'])
|
||||
|
||||
@@ -69,7 +69,17 @@ Rating rules
|
||||
Rating rules are the expressions used to create a charge (assign a value to
|
||||
a computing resource consumption). Rating rules can be created with
|
||||
PyScripts or with the use of fields, services and groups with hashmap
|
||||
rating rules.
|
||||
rating rules. You can define a ``start`` and ``end`` dates to the rating rules
|
||||
(PyScripts and hashmap mappings), which define the period they will be valid
|
||||
and applied in the rating process. If neither ``start`` or ``end`` dates are
|
||||
defined, they will be set as the rule's creation date and ``None``
|
||||
respectively. A ``None`` ``end`` date means the rating rule will last
|
||||
indefinitely. The ``start`` date cannot be set as a past date, if you really
|
||||
need to do it, you can force it using a ``force`` flag when creating the rule.
|
||||
Once the rule starts running (rule's ``start`` > current date), you will not be
|
||||
able to update the rule anymore, in this case, if you need to change the
|
||||
rule's value, you will need to delete it and create it again with the new
|
||||
value.
|
||||
|
||||
If we have a hashmap mapping configuration for a service and another
|
||||
hashmap map configuration for a field that belongs to the same service,
|
||||
|
||||
@@ -242,15 +242,18 @@ instance to a cost of 0.01:
|
||||
--value 93195dd4-bbf3-4b13-929d-8293ae72e056 \
|
||||
-g 9a2ff37d-be86-4642-8b7d-567bace61f06 \
|
||||
-t flat
|
||||
+--------------------------------------+--------------------------------------+------------+------+--------------------------------------+------------+--------------------------------------+------------+
|
||||
| Mapping ID | Value | Cost | Type | Field ID | Service ID | Group ID | Project ID |
|
||||
+--------------------------------------+--------------------------------------+------------+------+--------------------------------------+------------+--------------------------------------+------------+
|
||||
| 9c2418dc-99d3-44b6-8fdf-e9fa02f3ceb5 | 93195dd4-bbf3-4b13-929d-8293ae72e056 | 0.01000000 | flat | 18aa50b6-6da8-4c47-8a1f-43236b971625 | None | 9a2ff37d-be86-4642-8b7d-567bace61f06 | None |
|
||||
+--------------------------------------+--------------------------------------+------------+------+--------------------------------------+------------+--------------------------------------+------------+
|
||||
+--------------------------------------+--------------------------------------+---------------------+---------------------+------+------+-------------+---------+----------------------------------+------------+------------+------------+------+--------------------------------------+------------+--------------------------------------+------------+
|
||||
| Mapping ID | Value | Created At | Start | End | Name | Description | Deleted | Created By | Updated By | Deleted By | Cost | Type | Field ID | Service ID | Group ID | Project ID |
|
||||
+--------------------------------------+--------------------------------------+---------------------+---------------------+------+------+-------------+---------+----------------------------------+------------+------------+------------+------+--------------------------------------+------------+--------------------------------------+------------+
|
||||
| 9c2418dc-99d3-44b6-8fdf-e9fa02f3ceb5 | 93195dd4-bbf3-4b13-929d-8293ae72e056 | 2023-01-01T10:00:00 | 2023-01-01T10:00:00 | None | None | None | None | 7977999e2e2511e6a8b2df30b233ffcb | None | None | 0.01000000 | flat | 18aa50b6-6da8-4c47-8a1f-43236b971625 | None | 9a2ff37d-be86-4642-8b7d-567bace61f06 | None |
|
||||
+--------------------------------------+--------------------------------------+---------------------+---------------------+------+------+-------------+---------+----------------------------------+------------+------------+------------+------+--------------------------------------+------------+--------------------------------------+------------+
|
||||
|
||||
|
||||
In this example every machine in any project with the flavor m1.tiny will be
|
||||
rated 0.01 per collection period.
|
||||
rated 0.01 for the collection period from 2023-01-01T10:00:00 onwards. To use
|
||||
a custom period (``start`` and ``end``) that the hashmap mapping will be valid,
|
||||
you can use the parameters ``--start`` and ``--end``. To use ``start`` value in
|
||||
the past, use the ``--force`` parameter.
|
||||
|
||||
|
||||
Volume per GiB with discount
|
||||
@@ -295,11 +298,11 @@ Now let's setup the price per gigabyte:
|
||||
$ cloudkitty hashmap mapping create 0.001 \
|
||||
-s 74ad7e4e-9cae-45a8-884b-368a92803afe \
|
||||
-t flat -g 9736bbc0-8888-4700-96fc-58db5fded493
|
||||
+--------------------------------------+-------+------------+------+----------+--------------------------------------+--------------------------------------+------------+
|
||||
| Mapping ID | Value | Cost | Type | Field ID | Service ID | Group ID | Project ID |
|
||||
+--------------------------------------+-------+------------+------+----------+--------------------------------------+--------------------------------------+------------+
|
||||
| 09e36b13-ce89-4bd0-bbf1-1b80577031e8 | None | 0.00100000 | flat | None | 74ad7e4e-9cae-45a8-884b-368a92803afe | 9736bbc0-8888-4700-96fc-58db5fded493 | None |
|
||||
+--------------------------------------+-------+------------+------+----------+--------------------------------------+--------------------------------------+------------+
|
||||
+--------------------------------------+-------+---------------------+---------------------+------+------+-------------+---------+----------------------------------+------------+------------+------------+------+----------+--------------------------------------+--------------------------------------+------------+
|
||||
| Mapping ID | Value | Created At | Start | End | Name | Description | Deleted | Created By | Updated By | Deleted By | Cost | Type | Field ID | Service ID | Group ID | Project ID |
|
||||
+--------------------------------------+-------+---------------------+---------------------+------+------+-------------+---------+----------------------------------+------------+------------+------------+------+----------+--------------------------------------+--------------------------------------+------------+
|
||||
| 09e36b13-ce89-4bd0-bbf1-1b80577031e8 | None | 2023-01-01T10:00:00 | 2023-01-01T10:00:00 | None | None | None | None | 7977999e2e2511e6a8b2df30b233ffcb | None | None | 0.00100000 | flat | None | 74ad7e4e-9cae-45a8-884b-368a92803afe | 9736bbc0-8888-4700-96fc-58db5fded493 | None |
|
||||
+--------------------------------------+-------+---------------------+---------------------+------+------+-------------+---------+----------------------------------+------------+------------+------------+------+----------+--------------------------------------+--------------------------------------+------------+
|
||||
|
||||
|
||||
We have the basic price per gigabyte be we now want to apply a discount on huge
|
||||
|
||||
@@ -114,21 +114,25 @@ To use your script for rating, you will need to enable the pyscripts module
|
||||
Adding the script to CloudKitty
|
||||
-------------------------------
|
||||
|
||||
Create the script and specify its name.
|
||||
Create the script and specify its name, description, start and end dates.
|
||||
If the ``start`` and ``end`` are not given, the ``start`` will be set as the
|
||||
creation date and the ``end`` as ``None``. The script is valid from the
|
||||
``start`` time until the ``end`` time, if the ``end`` time is ``None``, the
|
||||
script is endless.
|
||||
|
||||
.. code-block:: console
|
||||
|
||||
$ cloudkitty pyscript create my_awesome_script script.py
|
||||
+-------------------+--------------------------------------+------------------------------------------+---------------------------------------+
|
||||
| Name | Script ID | Checksum | Data |
|
||||
+-------------------+--------------------------------------+------------------------------------------+---------------------------------------+
|
||||
| my_awesome_script | 78e1955a-4e7e-47e3-843c-524d8e6ad4c4 | 49e889018eb86b2035437ebb69093c0b6379f18c | from __future__ import print_function |
|
||||
| | | | from cloudkitty import rating |
|
||||
| | | | |
|
||||
| | | | import decimal |
|
||||
| | | | |
|
||||
| | | | {...} |
|
||||
| | | | |
|
||||
| | | | data = process(data) |
|
||||
| | | | |
|
||||
+-------------------+--------------------------------------+------------------------------------------+---------------------------------------+
|
||||
+-------------------+--------------------------------------+---------------------+---------------------+------+-------------+---------+----------------------------------+------------+------------+------------------------------------------+---------------------------------------+
|
||||
| Name | Script ID | Created At | Start | End | Description | Deleted | Created By | Updated By | Deleted By | Checksum | Data |
|
||||
+-------------------+--------------------------------------+---------------------+---------------------+------+-------------+---------+----------------------------------+------------+------------+------------------------------------------+---------------------------------------+
|
||||
| my_awesome_script | 78e1955a-4e7e-47e3-843c-524d8e6ad4c4 | 2023-01-01T10:00:00 | 2023-01-01T10:00:00 | None | None | None | 7977999e2e2511e6a8b2df30b233ffcb | None | None | 49e889018eb86b2035437ebb69093c0b6379f18c | from __future__ import print_function |
|
||||
| | | | | | | | | | | | from cloudkitty import rating |
|
||||
| | | | | | | | | | | | |
|
||||
| | | | | | | | | | | | import decimal |
|
||||
| | | | | | | | | | | | |
|
||||
| | | | | | | | | | | | {...} |
|
||||
| | | | | | | | | | | | |
|
||||
| | | | | | | | | | | | data = process(data) |
|
||||
| | | | | | | | | | | | |
|
||||
+-------------------+--------------------------------------+---------------------+---------------------+------+-------------+---------+----------------------------------+------------+------------+------------------------------------------+---------------------------------------+
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
---
|
||||
features:
|
||||
- |
|
||||
Add new fields to specify the validity period of rating rules for modules
|
||||
``hashmap`` and ``pyscripts``. These new fields also improve the audit
|
||||
mechanism for rating rules changes and deletion, allowing users to know
|
||||
when the rules were changed or removed (marked as deleted) and by whom.
|
||||
|
||||
upgrade:
|
||||
- |
|
||||
New rules for both ``hashmap`` and ``pyscript`` modules will no longer be
|
||||
reprocessed for past periods, by default they are valid from the moment
|
||||
they were created on. To allow new rules to be valid for past periods,
|
||||
operators will need to specify a ``start_date`` in the past and pass the
|
||||
query parameter ``force=true`` when creating the rating rule, then the
|
||||
rule will be valid and used since the defined start date. Update rating
|
||||
rules for both ``hashmap`` and ``pyscript`` modules will no longer be
|
||||
allowed for rules where the ``start_date`` is in the past, to do so, you
|
||||
will need to delete and create a new rating rule with the desired values,
|
||||
the unique value you can update for rules that already started, is the
|
||||
``end_date``.
|
||||
Reference in New Issue
Block a user