Merge "Added new mapping type: threshold"

This commit is contained in:
Jenkins 2015-05-07 07:40:56 +00:00 committed by Gerrit Code Review
commit f072724991
13 changed files with 1142 additions and 153 deletions

View File

@ -16,7 +16,6 @@
# @author: Stéphane Albert
#
from oslo.utils import uuidutils
from wsme import exc
from wsme import types as wtypes
from cloudkitty.i18n import _LE
@ -30,7 +29,7 @@ class UuidType(wtypes.UuidType):
@staticmethod
def validate(value):
if not uuidutils.is_uuid_like(value):
raise exc.InvalidType(_LE("Invalid UUID, got '%s'") % value)
raise ValueError(_LE("Invalid UUID, got '%s'") % value)
return value

View File

@ -62,7 +62,7 @@ class DBCommand(object):
try:
module = self.rating_models[name]
mod_migration = module.get_migration()
except IndexError:
except KeyError:
raise ModuleNotFound(name)
return mod_migration

View File

@ -15,6 +15,8 @@
#
# @author: Stéphane Albert
#
import decimal
from cloudkitty.openstack.common import log as logging
from cloudkitty import rating
from cloudkitty.rating.hash.controllers import root as root_api
@ -39,8 +41,7 @@ class HashMap(rating.RatingProcessorBase):
def __init__(self, tenant_id=None):
super(HashMap, self).__init__(tenant_id)
self._service_mappings = {}
self._field_mappings = {}
self._entries = {}
self._res = {}
self._load_rates()
@ -61,88 +62,137 @@ class HashMap(rating.RatingProcessorBase):
group_name = '_DEFAULT_'
if group_name not in mappings:
mappings[group_name] = {}
current_scope = mappings[group_name]
mapping_value = mapping_db.value
map_dict = {}
map_dict['cost'] = mapping_db.cost
map_dict['type'] = mapping_db.map_type
if mapping_value:
mappings[group_name][mapping_value] = map_dict
else:
mappings[group_name] = map_dict
current_scope[mapping_value] = {}
current_scope = current_scope[mapping_value]
current_scope['type'] = mapping_db.map_type
current_scope['cost'] = mapping_db.cost
return mappings
def _load_service_mappings(self, service_name, service_uuid):
def _load_thresholds(self, thresholds_uuid_list):
hashmap = hash_db_api.get_instance()
mappings_uuid_list = hashmap.list_mappings(service_uuid=service_uuid)
mappings = self._load_mappings(mappings_uuid_list)
if mappings:
self._service_mappings[service_name] = mappings
thresholds = {}
for threshold_uuid in thresholds_uuid_list:
threshold_db = hashmap.get_threshold(uuid=threshold_uuid)
if threshold_db.group_id:
group_name = threshold_db.group.name
else:
group_name = '_DEFAULT_'
if group_name not in thresholds:
thresholds[group_name] = {}
current_scope = thresholds[group_name]
def _load_field_mappings(self, service_name, field_name, field_uuid):
threshold_level = threshold_db.level
current_scope[threshold_level] = {}
current_scope = current_scope[threshold_level]
current_scope['type'] = threshold_db.map_type
current_scope['cost'] = threshold_db.cost
return thresholds
def _load_service_entries(self, service_name, service_uuid):
hashmap = hash_db_api.get_instance()
self._entries[service_name] = {}
mappings_uuid_list = hashmap.list_mappings(
service_uuid=service_uuid)
mappings = self._load_mappings(mappings_uuid_list)
self._entries[service_name]['mappings'] = mappings
thresholds_uuid_list = hashmap.list_thresholds(
service_uuid=service_uuid)
thresholds = self._load_thresholds(thresholds_uuid_list)
self._entries[service_name]['thresholds'] = thresholds
def _load_field_entries(self, service_name, field_name, field_uuid):
hashmap = hash_db_api.get_instance()
mappings_uuid_list = hashmap.list_mappings(field_uuid=field_uuid)
mappings = self._load_mappings(mappings_uuid_list)
if mappings:
self._field_mappings[service_name] = {}
self._field_mappings[service_name][field_name] = mappings
thresholds_uuid_list = hashmap.list_thresholds(field_uuid=field_uuid)
thresholds = self._load_thresholds(thresholds_uuid_list)
if service_name not in self._entries:
self._entries[service_name] = {}
if 'fields' not in self._entries[service_name]:
self._entries[service_name]['fields'] = {}
scope = self._entries[service_name]['fields'][field_name] = {}
scope['mappings'] = mappings
scope['thresholds'] = thresholds
def _load_rates(self):
self._service_mappings = {}
self._field_mappings = {}
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_mappings(service_name, service_uuid)
self._load_service_entries(service_name, service_uuid)
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_mappings(service_name, field_name, field_uuid)
self._load_field_entries(service_name, field_name, field_uuid)
def add_rating_informations(self, data):
if 'rating' not in data:
data['rating'] = {'price': 0}
for entry in self._res.values():
res = entry['rate'] * entry['flat']
data['rating']['price'] += res * data['vol']['qty']
rate = entry['rate']
flat = entry['flat']
if entry['threshold']['scope'] == 'field':
if entry['threshold']['type'] == 'flat':
flat += entry['threshold']['cost']
else:
rate *= entry['threshold']['cost']
res = rate * flat
res *= data['vol']['qty']
if entry['threshold']['scope'] == 'service':
if entry['threshold']['type'] == 'flat':
res += entry['threshold']['cost']
else:
res *= entry['threshold']['cost']
data['rating']['price'] += res
def update_result(self, group, map_type, value):
def update_result(self,
group,
map_type,
cost,
level=0,
is_threshold=False,
threshold_scope='field'):
if group not in self._res:
self._res[group] = {'flat': 0,
'rate': 1}
'rate': 1,
'threshold': {
'level': -1,
'cost': 0,
'type': 'flat',
'scope': 'field'}}
if is_threshold:
best = self._res[group]['threshold']['level']
if level > best:
self._res[group]['threshold']['level'] = level
self._res[group]['threshold']['cost'] = cost
self._res[group]['threshold']['type'] = map_type
self._res[group]['threshold']['scope'] = threshold_scope
else:
if map_type == 'rate':
self._res[group]['rate'] *= value
self._res[group]['rate'] *= cost
elif map_type == 'flat':
new_flat = value
new_flat = cost
cur_flat = self._res[group]['flat']
if new_flat > cur_flat:
self._res[group]['flat'] = new_flat
def process_service_map(self, service_name, data):
if service_name not in self._service_mappings:
return
serv_map = self._service_mappings[service_name]
for group_name, mapping in serv_map.items():
self.update_result(group_name,
mapping['type'],
mapping['cost'])
def process_field_map(self, service_name, data):
if service_name not in self._field_mappings:
return {}
field_map = self._field_mappings[service_name]
desc_data = data['desc']
for field_name, group_mappings in field_map.items():
if field_name not in desc_data:
continue
for group_name, mappings in group_mappings.items():
mapping_default = mappings.pop('_DEFAULT_', {})
def process_mappings(self,
mapping_groups,
cmp_value):
for group_name, mappings in mapping_groups.items():
mapping_default = mappings.get('_DEFAULT_', {})
matched = False
for mapping_value, mapping in mappings.items():
if desc_data[field_name] == mapping_value:
if mapping is mapping_default:
continue
if cmp_value == mapping_value:
self.update_result(
group_name,
mapping['type'],
@ -154,13 +204,69 @@ class HashMap(rating.RatingProcessorBase):
mapping_default['type'],
mapping_default['cost'])
def process_thresholds(self,
threshold_groups,
cmp_level,
threshold_type):
for group_name, thresholds in threshold_groups.items():
threshold_default = thresholds.get('_DEFAULT_', {})
matched = False
for threshold_level, threshold in thresholds.items():
if threshold is threshold_default:
continue
if cmp_level >= threshold_level:
self.update_result(
group_name,
threshold['type'],
threshold['cost'],
threshold_level,
True,
threshold_type)
matched = True
if not matched and threshold_default:
self.update_result(
group_name,
threshold_default['type'],
threshold_default['cost'],
True,
threshold_type)
def process_services(self, service_name, data):
if service_name not in self._entries:
return
service_mappings = self._entries[service_name]['mappings']
for group_name, mapping in service_mappings.items():
self.update_result(group_name,
mapping['type'],
mapping['cost'])
service_thresholds = self._entries[service_name]['thresholds']
self.process_thresholds(service_thresholds,
data['vol']['qty'],
'service')
def process_fields(self, service_name, data):
if service_name not in self._entries:
return
desc_data = data['desc']
field_mappings = self._entries[service_name]['fields']
for field_name, group_mappings in field_mappings.items():
if field_name not in desc_data:
continue
cmp_value = desc_data[field_name]
self.process_mappings(group_mappings['mappings'],
cmp_value)
if group_mappings['thresholds']:
self.process_thresholds(group_mappings['thresholds'],
decimal.Decimal(cmp_value),
'field')
def process(self, data):
for cur_data in data:
cur_usage = cur_data['usage']
for service_name, service_data in cur_usage.items():
for item in service_data:
self._res = {}
self.process_service_map(service_name, item)
self.process_field_map(service_name, item)
self.process_services(service_name, item)
self.process_fields(service_name, item)
self.add_rating_informations(item)
return data

View File

@ -23,6 +23,7 @@ from cloudkitty.rating.hash.controllers import field as field_api
from cloudkitty.rating.hash.controllers import group as group_api
from cloudkitty.rating.hash.controllers import mapping as mapping_api
from cloudkitty.rating.hash.controllers import service as service_api
from cloudkitty.rating.hash.controllers import threshold as threshold_api
from cloudkitty.rating.hash.datamodels import mapping as mapping_models
@ -39,6 +40,7 @@ class HashMapConfigController(rest.RestController):
fields = field_api.HashMapFieldsController()
groups = group_api.HashMapGroupsController()
mappings = mapping_api.HashMapMappingsController()
thresholds = threshold_api.HashMapThresholdsController()
@wsme_pecan.wsexpose([wtypes.text])
def get_types(self):

View File

@ -0,0 +1,163 @@
# -*- coding: utf-8 -*-
# Copyright 2015 Objectif Libre
#
# 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.
#
# @author: Stéphane Albert
#
import pecan
from pecan import rest
import wsmeext.pecan as wsme_pecan
from cloudkitty.api.v1 import types as ck_types
from cloudkitty.rating.hash.datamodels import group as group_models
from cloudkitty.rating.hash.datamodels import threshold as threshold_models
from cloudkitty.rating.hash.db import api as db_api
class HashMapThresholdsController(rest.RestController):
"""Controller responsible of thresholds management.
"""
_custom_actions = {
'group': ['GET']
}
@wsme_pecan.wsexpose(group_models.Group,
ck_types.UuidType())
def group(self, threshold_id):
"""Get the group attached to the threshold.
:param threshold_id: UUID of the threshold to filter on.
"""
hashmap = db_api.get_instance()
try:
group_db = hashmap.get_group_from_threshold(
uuid=threshold_id)
return group_models.Group(**group_db.export_model())
except db_api.ThresholdHasNoGroup as e:
pecan.abort(404, str(e))
@wsme_pecan.wsexpose(threshold_models.ThresholdCollection,
ck_types.UuidType(),
ck_types.UuidType(),
ck_types.UuidType(),
bool,
status_code=200)
def get_all(self,
service_id=None,
field_id=None,
group_id=None,
no_group=False):
"""Get the threshold list
:param service_id: Service UUID to filter on.
:param field_id: Field UUID to filter on.
:param group_id: Group UUID to filter on.
:param no_group: Filter on orphaned thresholds.
:return: List of every thresholds.
"""
hashmap = db_api.get_instance()
threshold_list = []
thresholds_uuid_list = hashmap.list_thresholds(service_uuid=service_id,
field_uuid=field_id,
group_uuid=group_id)
for threshold_uuid in thresholds_uuid_list:
threshold_db = hashmap.get_threshold(uuid=threshold_uuid)
threshold_list.append(threshold_models.Threshold(
**threshold_db.export_model()))
res = threshold_models.ThresholdCollection(thresholds=threshold_list)
return res
@wsme_pecan.wsexpose(threshold_models.Threshold,
ck_types.UuidType())
def get_one(self, threshold_id):
"""Return a threshold.
:param threshold_id: UUID of the threshold to filter on.
"""
hashmap = db_api.get_instance()
try:
threshold_db = hashmap.get_threshold(uuid=threshold_id)
return threshold_models.Threshold(
**threshold_db.export_model())
except db_api.NoSuchThreshold as e:
pecan.abort(400, str(e))
@wsme_pecan.wsexpose(threshold_models.Threshold,
body=threshold_models.Threshold,
status_code=201)
def post(self, threshold_data):
"""Create a threshold.
:param threshold_data: Informations about the threshold to create.
"""
hashmap = db_api.get_instance()
try:
threshold_db = hashmap.create_threshold(
level=threshold_data.level,
map_type=threshold_data.map_type,
cost=threshold_data.cost,
field_id=threshold_data.field_id,
group_id=threshold_data.group_id,
service_id=threshold_data.service_id)
pecan.response.location = pecan.request.path_url
if pecan.response.location[-1] != '/':
pecan.response.location += '/'
pecan.response.location += threshold_db.threshold_id
return threshold_models.Threshold(
**threshold_db.export_model())
except db_api.ThresholdAlreadyExists as e:
pecan.abort(409, str(e))
@wsme_pecan.wsexpose(None,
ck_types.UuidType(),
body=threshold_models.Threshold,
status_code=302)
def put(self, threshold_id, threshold):
"""Update a threshold.
:param threshold_id: UUID of the threshold to update.
:param threshold: Threshold data to insert.
"""
hashmap = db_api.get_instance()
try:
hashmap.update_threshold(
threshold_id,
threshold_id=threshold.threshold_id,
level=threshold.level,
cost=threshold.cost,
map_type=threshold.map_type,
group_id=threshold.group_id)
pecan.response.headers['Location'] = pecan.request.path
except (db_api.NoSuchService,
db_api.NoSuchField,
db_api.NoSuchThreshold) as e:
pecan.abort(400, str(e))
@wsme_pecan.wsexpose(None,
ck_types.UuidType(),
status_code=204)
def delete(self, threshold_id):
"""Delete a threshold.
:param threshold_id: UUID of the threshold to delete.
"""
hashmap = db_api.get_instance()
try:
hashmap.delete_threshold(uuid=threshold_id)
except (db_api.NoSuchService,
db_api.NoSuchField,
db_api.NoSuchThreshold) as e:
pecan.abort(400, str(e))

View File

@ -32,12 +32,10 @@ class Mapping(wtypes.Base):
directly apply the rate to the volume.
"""
mapping_id = wtypes.wsattr(ck_types.UuidType(),
mandatory=False,
readonly=True)
mapping_id = wtypes.wsattr(ck_types.UuidType(), mandatory=False)
"""UUID of the mapping."""
value = wtypes.wsattr(wtypes.text, mandatory=False)
value = wtypes.wsattr(wtypes.text, mandatory=False, default='')
"""Key of the mapping."""
map_type = wtypes.wsattr(MAP_TYPE, default='flat', name='type')

View File

@ -0,0 +1,83 @@
# -*- coding: utf-8 -*-
# Copyright 2015 Objectif Libre
#
# 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.
#
# @author: Stéphane Albert
#
import decimal
from wsme import types as wtypes
from cloudkitty.api.v1 import types as ck_types
from cloudkitty.rating.hash.datamodels import mapping as mapping_models
class Threshold(wtypes.Base):
"""Type describing a Threshold.
A threshold is used to apply rating rules based on a level, if the parent
is a field then the level is checked against a metadata. If it's a service
then it's the quantity of the resource that is checked.
"""
threshold_id = wtypes.wsattr(ck_types.UuidType(), mandatory=False)
"""UUID of the threshold."""
level = wtypes.wsattr(decimal.Decimal,
mandatory=True,
default=decimal.Decimal('0'))
"""Level of the threshold."""
map_type = wtypes.wsattr(mapping_models.MAP_TYPE,
default='flat',
name='type')
"""Type of the threshold."""
cost = wtypes.wsattr(decimal.Decimal, mandatory=True)
"""Value of the threshold."""
service_id = wtypes.wsattr(ck_types.UuidType(),
mandatory=False)
"""UUID of the service."""
field_id = wtypes.wsattr(ck_types.UuidType(),
mandatory=False)
"""UUID of the field."""
group_id = wtypes.wsattr(ck_types.UuidType(),
mandatory=False)
"""UUID of the hashmap group."""
@classmethod
def sample(cls):
sample = cls(threshold_id='39dbd39d-f663-4444-a795-fb19d81af136',
field_id='ac55b000-a05b-4832-b2ff-265a034886ab',
level=decimal.Decimal('1024'),
map_type='flat',
cost=decimal.Decimal('4.2'))
return sample
class ThresholdCollection(wtypes.Base):
"""Type describing a list of mappings.
"""
thresholds = [Threshold]
"""List of thresholds."""
@classmethod
def sample(cls):
sample = Threshold.sample()
return cls(thresholds=[sample])

View File

@ -70,6 +70,15 @@ class NoSuchMapping(Exception):
self.uuid = uuid
class NoSuchThreshold(Exception):
"""Raised when the threshold doesn't exist."""
def __init__(self, uuid):
msg = ("No such threshold: %s" % uuid)
super(NoSuchThreshold, self).__init__(msg)
self.uuid = uuid
class NoSuchType(Exception):
"""Raised when a mapping type is not handled."""
@ -120,6 +129,16 @@ class MappingAlreadyExists(Exception):
self.uuid = uuid
class ThresholdAlreadyExists(Exception):
"""Raised when the threshold already exists."""
def __init__(self, threshold, uuid):
super(ThresholdAlreadyExists, self).__init__(
"Threshold %s already exists (UUID: %s)" % (threshold, uuid))
self.threshold = threshold
self.uuid = uuid
class MappingHasNoGroup(Exception):
"""Raised when the mapping is not attached to a group."""
@ -129,6 +148,15 @@ class MappingHasNoGroup(Exception):
self.uuid = uuid
class ThresholdHasNoGroup(Exception):
"""Raised when the threshold is not attached to a group."""
def __init__(self, uuid):
super(ThresholdHasNoGroup, self).__init__(
"Threshold has no group (UUID: %s)" % uuid)
self.uuid = uuid
@six.add_metaclass(abc.ABCMeta)
class HashMap(object):
"""Base class for hashmap configuration."""
@ -170,6 +198,13 @@ class HashMap(object):
:param uuid: UUID of the mapping to get.
"""
@abc.abstractmethod
def get_threshold(self, uuid):
"""Return a threshold object.
:param uuid: UUID of the threshold to get.
"""
@abc.abstractmethod
def list_services(self):
"""Return an UUID list of every service.
@ -205,6 +240,22 @@ class HashMap(object):
:return list(str): List of mappings' UUID.
"""
@abc.abstractmethod
def list_thresholds(self,
service_uuid=None,
field_uuid=None,
group_uuid=None,
no_group=False):
"""Return an UUID list of every threshold.
:param service_uuid: The service to filter on.
:param field_uuid: The field to filter on.
:param group_uuid: The group to filter on.
:param no_group: Filter on thresholds without a group.
:return list(str): List of thresholds' UUID.
"""
@abc.abstractmethod
def create_service(self, name):
"""Create a new service.
@ -245,6 +296,24 @@ class HashMap(object):
:param group_id: The group of calculations to apply.
"""
@abc.abstractmethod
def create_threshold(self,
cost,
map_type='rate',
level=None,
service_id=None,
field_id=None,
group_id=None):
"""Create a new service/field threshold.
:param cost: Rating value to apply to this threshold.
:param map_type: The type of rating rule.
:param level: Level of the field this threshold is applying to.
:param service_id: Service the threshold is applying to.
:param field_id: Field the threshold is applying to.
:param group_id: The group of calculations to apply.
"""
@abc.abstractmethod
def update_mapping(self, uuid, **kwargs):
"""Update a mapping.
@ -256,6 +325,17 @@ class HashMap(object):
:param group_id: The group of calculations to apply.
"""
@abc.abstractmethod
def update_threshold(self, uuid, **kwargs):
"""Update a mapping.
:param uuid UUID of the threshold to modify.
:param cost: Rating value to apply to this threshold.
:param map_type: The type of rating rule.
:param level: Level of the field this threshold is applying to.
:param group_id: The group of calculations to apply.
"""
@abc.abstractmethod
def delete_service(self, name=None, uuid=None):
"""Delete a service recursively.
@ -284,3 +364,9 @@ class HashMap(object):
:param uuid: UUID of the mapping to delete.
"""
@abc.abstractmethod
def delete_threshold(self, uuid):
"""Delete a threshold
:param uuid: UUID of the threshold to delete.
"""

View File

@ -0,0 +1,44 @@
"""Added threshold support.
Revision ID: 4fa888fd7eda
Revises: 3dd7e13527f3
Create Date: 2015-05-05 14:39:24.562388
"""
# revision identifiers, used by Alembic.
revision = '4fa888fd7eda'
down_revision = '3dd7e13527f3'
from alembic import op
import sqlalchemy as sa
def upgrade():
op.create_table('hashmap_thresholds',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('threshold_id', sa.String(length=36), nullable=False),
sa.Column('level', sa.Numeric(precision=20, scale=8), nullable=True),
sa.Column('cost', sa.Numeric(precision=20, scale=8), nullable=False),
sa.Column('map_type', sa.Enum('flat', 'rate', name='enum_map_type'),
nullable=False),
sa.Column('service_id', sa.Integer(), nullable=True),
sa.Column('field_id', sa.Integer(), nullable=True),
sa.Column('group_id', sa.Integer(), nullable=True),
sa.ForeignKeyConstraint(['field_id'], ['hashmap_fields.id'],
ondelete='CASCADE'),
sa.ForeignKeyConstraint(['group_id'], ['hashmap_groups.id'],
ondelete='SET NULL'),
sa.ForeignKeyConstraint(['service_id'], ['hashmap_services.id'],
ondelete='CASCADE'),
sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint('threshold_id'),
sa.UniqueConstraint('level', 'field_id', name='uniq_field_mapping'),
sa.UniqueConstraint('level', 'service_id', name='uniq_service_mapping'),
mysql_charset='utf8',
mysql_engine='InnoDB'
)
def downgrade():
op.drop_table('hashmap_thresholds')

View File

@ -99,6 +99,17 @@ class HashMap(api.HashMap):
except sqlalchemy.orm.exc.NoResultFound:
raise api.NoSuchMapping(uuid)
def get_threshold(self, uuid):
session = db.get_session()
try:
q = session.query(models.HashMapThreshold)
q = q.filter(
models.HashMapThreshold.threshold_id == uuid)
res = q.one()
return res
except sqlalchemy.orm.exc.NoResultFound:
raise api.NoSuchThreshold(uuid)
def get_group_from_mapping(self, uuid):
session = db.get_session()
try:
@ -112,6 +123,19 @@ class HashMap(api.HashMap):
except sqlalchemy.orm.exc.NoResultFound:
raise api.MappingHasNoGroup(uuid=uuid)
def get_group_from_threshold(self, uuid):
session = db.get_session()
try:
q = session.query(models.HashMapGroup)
q = q.join(
models.HashMapGroup.thresholds)
q = q.filter(
models.HashMapThreshold.threshold_id == uuid)
res = q.one()
return res
except sqlalchemy.orm.exc.NoResultFound:
raise api.ThresholdHasNoGroup(uuid=uuid)
def list_services(self):
session = db.get_session()
q = session.query(models.HashMapService)
@ -163,8 +187,37 @@ class HashMap(api.HashMap):
elif no_group:
q = q.filter(models.HashMapMapping.group_id == None) # noqa
res = q.values(
models.HashMapMapping.mapping_id
)
models.HashMapMapping.mapping_id)
return [uuid[0] for uuid in res]
def list_thresholds(self,
service_uuid=None,
field_uuid=None,
group_uuid=None,
no_group=False):
session = db.get_session()
q = session.query(models.HashMapThreshold)
if service_uuid:
q = q.join(
models.HashMapThreshold.service)
q = q.filter(
models.HashMapService.service_id == service_uuid)
elif field_uuid:
q = q.join(
models.HashMapThreshold.field)
q = q.filter(models.HashMapField.field_id == field_uuid)
if group_uuid:
q = q.join(
models.HashMapThreshold.group)
q = q.filter(models.HashMapGroup.group_id == group_uuid)
elif not service_uuid and not field_uuid:
raise ValueError('You must specify either service_uuid,'
' field_uuid or group_uuid.')
elif no_group:
q = q.filter(models.HashMapThreshold.group_id == None) # noqa
res = q.values(
models.HashMapThreshold.threshold_id)
return [uuid[0] for uuid in res]
def create_service(self, name):
@ -218,6 +271,12 @@ class HashMap(api.HashMap):
group_id=None):
if field_id and service_id:
raise ValueError('You can only specify one parent.')
if not value and not service_id:
raise ValueError('You must either specify a value'
' or a service_id')
elif value and service_id:
raise ValueError('You can\'t specify a value'
' and a service_id.')
field_fk = None
if field_id:
field_db = self.get_field(uuid=field_id)
@ -226,14 +285,10 @@ class HashMap(api.HashMap):
if service_id:
service_db = self.get_service(uuid=service_id)
service_fk = service_db.id
if not value and not service_id:
raise ValueError('You must either specify a value'
' or a service_id')
elif value and service_id:
raise ValueError('You can\'t specify a value'
' and a service_id')
group_fk = None
if group_id:
group_db = self.get_group(uuid=group_id)
group_fk = group_db.id
session = db.get_session()
try:
with session.begin():
@ -244,8 +299,8 @@ class HashMap(api.HashMap):
field_id=field_fk,
service_id=service_fk,
map_type=map_type)
if group_id:
field_map.group_id = group_db.id
if group_fk:
field_map.group_id = group_fk
session.add(field_map)
return field_map
except exception.DBDuplicateEntry:
@ -253,6 +308,46 @@ class HashMap(api.HashMap):
except exception.DBError:
raise api.NoSuchType(map_type)
def create_threshold(self,
level,
cost,
map_type='rate',
service_id=None,
field_id=None,
group_id=None):
if field_id and service_id:
raise ValueError('You can only specify one parent.')
field_fk = None
if field_id:
field_db = self.get_field(uuid=field_id)
field_fk = field_db.id
service_fk = None
if service_id:
service_db = self.get_service(uuid=service_id)
service_fk = service_db.id
group_fk = None
if group_id:
group_db = self.get_group(uuid=group_id)
group_fk = group_db.id
session = db.get_session()
try:
with session.begin():
threshold_db = models.HashMapThreshold(
threshold_id=uuidutils.generate_uuid(),
level=level,
cost=cost,
field_id=field_fk,
service_id=service_fk,
map_type=map_type)
if group_fk:
threshold_db.group_id = group_fk
session.add(threshold_db)
return threshold_db
except exception.DBDuplicateEntry:
raise api.ThresholdAlreadyExists(level, threshold_db.field_id)
except exception.DBError:
raise api.NoSuchType(map_type)
def update_mapping(self, uuid, **kwargs):
session = db.get_session()
try:
@ -286,16 +381,47 @@ class HashMap(api.HashMap):
except sqlalchemy.orm.exc.NoResultFound:
raise api.NoSuchMapping(uuid)
def update_threshold(self, uuid, **kwargs):
session = db.get_session()
try:
with session.begin():
q = session.query(models.HashMapThreshold)
q = q.filter(
models.HashMapThreshold.threshold_id == uuid)
threshold_db = q.with_lockmode('update').one()
if kwargs:
# Resolve FK
if 'group_id' in kwargs:
group_id = kwargs.pop('group_id')
if group_id:
group_db = self.get_group(group_id)
threshold_db.group_id = group_db.id
# Service and Field shouldn't be updated
excluded_cols = ['threshold_id', 'service_id', 'field_id']
for col in excluded_cols:
if col in kwargs:
kwargs.pop(col)
for attribute, value in six.iteritems(kwargs):
if hasattr(threshold_db, attribute):
setattr(threshold_db, attribute, value)
else:
raise ValueError('No such attribute: {}'.format(
attribute))
else:
raise ValueError('No attribute to update.')
return threshold_db
except sqlalchemy.orm.exc.NoResultFound:
raise api.NoSuchThreshold(uuid)
def delete_service(self, name=None, uuid=None):
session = db.get_session()
q = utils.model_query(
models.HashMapService,
session
)
session)
if name:
q = q.filter_by(name=name)
q = q.filter(models.HashMapService.name == name)
elif uuid:
q = q.filter_by(service_id=uuid)
q = q.filter(models.HashMapService.service_id == uuid)
else:
raise ValueError('You must specify either name or uuid.')
r = q.delete()
@ -306,11 +432,8 @@ class HashMap(api.HashMap):
session = db.get_session()
q = utils.model_query(
models.HashMapField,
session
)
q = q.filter_by(
field_id=uuid
)
session)
q = q.filter(models.HashMapField.field_id == uuid)
r = q.delete()
if not r:
raise api.NoSuchField(uuid)
@ -319,10 +442,8 @@ class HashMap(api.HashMap):
session = db.get_session()
q = utils.model_query(
models.HashMapGroup,
session
).filter_by(
group_id=uuid,
)
session)
q = q.filter(models.HashMapGroup.group_id == uuid)
with session.begin():
try:
r = q.with_lockmode('update').one()
@ -331,17 +452,26 @@ class HashMap(api.HashMap):
if recurse:
for mapping in r.mappings:
session.delete(mapping)
for threshold in r.thresholds:
session.delete(threshold)
q.delete()
def delete_mapping(self, uuid):
session = db.get_session()
q = utils.model_query(
models.HashMapMapping,
session
)
q = q.filter_by(
mapping_id=uuid
)
session)
q = q.filter(models.HashMapMapping.mapping_id == uuid)
r = q.delete()
if not r:
raise api.NoSuchMapping(uuid)
def delete_threshold(self, uuid):
session = db.get_session()
q = utils.model_query(
models.HashMapThreshold,
session)
q = q.filter(models.HashMapThreshold.threshold_id == uuid)
r = q.delete()
if not r:
raise api.NoSuchThreshold(uuid)

View File

@ -75,8 +75,7 @@ class HashMapService(Base, HashMapBase):
name = sqlalchemy.Column(
sqlalchemy.String(255),
nullable=False,
unique=True
)
unique=True)
fields = orm.relationship('HashMapField',
backref=orm.backref(
'service',
@ -85,6 +84,10 @@ class HashMapService(Base, HashMapBase):
backref=orm.backref(
'service',
lazy='immediate'))
thresholds = orm.relationship('HashMapThreshold',
backref=orm.backref(
'service',
lazy='immediate'))
def __repr__(self):
return ('<HashMapService[{uuid}]: '
@ -120,12 +123,15 @@ class HashMapField(Base, HashMapBase):
sqlalchemy.Integer,
sqlalchemy.ForeignKey('hashmap_services.id',
ondelete='CASCADE'),
nullable=False
)
nullable=False)
mappings = orm.relationship('HashMapMapping',
backref=orm.backref(
'field',
lazy='immediate'))
thresholds = orm.relationship('HashMapThreshold',
backref=orm.backref(
'field',
lazy='immediate'))
def __repr__(self):
return ('<HashMapField[{uuid}]: '
@ -152,6 +158,10 @@ class HashMapGroup(Base, HashMapBase):
backref=orm.backref(
'group',
lazy='immediate'))
thresholds = orm.relationship('HashMapThreshold',
backref=orm.backref(
'group',
lazy='immediate'))
def __repr__(self):
return ('<HashMapGroup[{uuid}]: '
@ -211,3 +221,56 @@ class HashMapMapping(Base, HashMapBase):
map_type=self.map_type,
value=self.value,
cost=self.cost)
class HashMapThreshold(Base, HashMapBase):
"""A threshold matching a service, a field with a level and a type.
"""
__tablename__ = 'hashmap_thresholds'
fk_to_resolve = {'service_id': 'service.service_id',
'field_id': 'field.field_id',
'group_id': 'group.group_id'}
@declarative.declared_attr
def __table_args__(cls):
args = (schema.UniqueConstraint('level', 'field_id',
name='uniq_field_mapping'),
schema.UniqueConstraint('level', 'service_id',
name='uniq_service_mapping'),
HashMapBase.__table_args__,)
return args
id = sqlalchemy.Column(sqlalchemy.Integer,
primary_key=True)
threshold_id = sqlalchemy.Column(sqlalchemy.String(36),
nullable=False,
unique=True)
level = sqlalchemy.Column(sqlalchemy.Numeric(20, 8),
nullable=True)
cost = sqlalchemy.Column(sqlalchemy.Numeric(20, 8),
nullable=False)
map_type = sqlalchemy.Column(sqlalchemy.Enum('flat',
'rate',
name='enum_map_type'),
nullable=False)
service_id = sqlalchemy.Column(sqlalchemy.Integer,
sqlalchemy.ForeignKey('hashmap_services.id',
ondelete='CASCADE'),
nullable=True)
field_id = sqlalchemy.Column(sqlalchemy.Integer,
sqlalchemy.ForeignKey('hashmap_fields.id',
ondelete='CASCADE'),
nullable=True)
group_id = sqlalchemy.Column(sqlalchemy.Integer,
sqlalchemy.ForeignKey('hashmap_groups.id',
ondelete='SET NULL'),
nullable=True)
def __repr__(self):
return ('<HashMapThreshold[{uuid}]: '
'type={map_type} {level}={cost}>').format(
uuid=self.threshold_id,
map_type=self.map_type,
level=self.level,
cost=self.cost)

View File

@ -15,6 +15,7 @@
#
# @author: Stéphane Albert
#
import copy
import decimal
import mock
@ -61,7 +62,7 @@ CK_RESOURCES_DATA = [{
"user_id": "55b3379b949243009ee96972fbf51ed1",
"vcpus": "1"},
"vol": {
"qty": 1,
"qty": 2,
"unit": "instance"}},
{
"desc": {
@ -296,6 +297,21 @@ class HashMapRatingTest(tests.TestCase):
mappings = self._db_api.list_mappings(field_uuid=field_db.field_id)
self.assertEqual([mapping_db.mapping_id], mappings)
def test_get_mapping(self):
service_db = self._db_api.create_service('compute')
field_db = self._db_api.create_field(service_db.service_id,
'flavor')
mapping_db = self._db_api.create_mapping(
value='m1.tiny',
cost='1.337',
map_type='flat',
field_id=field_db.field_id)
mapping = self._db_api.get_mapping(mapping_db.mapping_id)
self.assertEqual('flat', mapping.map_type)
self.assertEqual('m1.tiny', mapping.value)
self.assertEqual(decimal.Decimal('1.337'), mapping.cost)
self.assertEqual(field_db.id, mapping.field_id)
def test_list_mappings_from_services(self):
service_db = self._db_api.create_service('compute')
mapping_db = self._db_api.create_mapping(
@ -390,37 +406,87 @@ class HashMapRatingTest(tests.TestCase):
mappings = self._db_api.list_mappings(field_uuid=field_db.field_id)
self.assertEqual([], mappings)
# Processing tests
def test_load_rates(self):
# Threshold tests
def test_create_threshold(self):
service_db = self._db_api.create_service('compute')
field_db = self._db_api.create_field(service_db.service_id,
'memory')
threshold_db = self._db_api.create_threshold(
level='64',
cost='0.1337',
map_type='flat',
field_id=field_db.field_id)
thresholds = self._db_api.list_thresholds(field_uuid=field_db.field_id)
self.assertEqual([threshold_db.threshold_id], thresholds)
def test_get_threshold(self):
service_db = self._db_api.create_service('compute')
field_db = self._db_api.create_field(service_db.service_id,
'memory')
threshold_db = self._db_api.create_threshold(
level='64',
cost='0.1337',
map_type='rate',
field_id=field_db.field_id)
threshold = self._db_api.get_threshold(threshold_db.threshold_id)
self.assertEqual('rate', threshold.map_type)
self.assertEqual(decimal.Decimal('64'), threshold.level)
self.assertEqual(decimal.Decimal('0.1337'), threshold.cost)
self.assertEqual(field_db.id, threshold.field_id)
# Processing tests
def _generate_hashmap_rules(self):
mapping_list = []
threshold_list = []
service_db = self._db_api.create_service('compute')
flavor_field = self._db_api.create_field(service_db.service_id,
'flavor')
memory_field = self._db_api.create_field(service_db.service_id,
'memory')
group_db = self._db_api.create_group('test_group')
mapping_list.append(
self._db_api.create_mapping(
cost='1.42',
map_type='rate',
service_id=service_db.service_id)
service_id=service_db.service_id))
mapping_list.append(
self._db_api.create_mapping(
value='m1.tiny',
cost='1.337',
map_type='flat',
field_id=field_db.field_id)
field_id=flavor_field.field_id))
mapping_list.append(
self._db_api.create_mapping(
value='m1.large',
cost='13.37',
map_type='rate',
field_id=field_db.field_id,
group_id=group_db.group_id)
self._hash.reload_config()
service_expect = {
'compute': {
'_DEFAULT_': {
'cost': decimal.Decimal('1.42'),
'type': 'rate'}}}
field_id=flavor_field.field_id,
group_id=group_db.group_id))
threshold_list.append(
self._db_api.create_threshold(
level='64',
cost='0.02',
map_type='flat',
field_id=memory_field.field_id,
group_id=group_db.group_id))
threshold_list.append(
self._db_api.create_threshold(
level='128',
cost='0.03',
map_type='flat',
field_id=memory_field.field_id,
group_id=group_db.group_id))
return ([mapping.mapping_id for mapping in mapping_list],
[threshold.threshold_id for threshold in threshold_list])
field_expect = {
def test_load_rates(self):
self._generate_hashmap_rules()
self._hash.reload_config()
expect = {
'compute': {
'fields': {
'flavor': {
'mappings': {
'_DEFAULT_': {
'm1.tiny': {
'cost': decimal.Decimal('1.337'),
@ -428,13 +494,82 @@ class HashMapRatingTest(tests.TestCase):
'test_group': {
'm1.large': {
'cost': decimal.Decimal('13.37'),
'type': 'rate'}}}}}
self.assertEqual(service_expect,
self._hash._service_mappings)
self.assertEqual(field_expect,
self._hash._field_mappings)
'type': 'rate'}}},
'thresholds': {}},
'memory': {
'mappings': {},
'thresholds': {
'test_group': {
64: {
'cost': decimal.Decimal('0.02'),
'type': 'flat'},
128: {
'cost': decimal.Decimal('0.03'),
'type': 'flat'}}}}},
'mappings': {
'_DEFAULT_': {
'cost': decimal.Decimal('1.42'),
'type': 'rate'}},
'thresholds': {}}}
self.assertEqual(expect,
self._hash._entries)
def test_process_service_map(self):
def test_load_mappings(self):
mapping_list = []
service_db = self._db_api.create_service('compute')
field_db = self._db_api.create_field(service_db.service_id,
'flavor')
group_db = self._db_api.create_group('test_group')
mapping_list.append(
self._db_api.create_mapping(
value='m1.tiny',
cost='1.337',
map_type='flat',
field_id=field_db.field_id))
mapping_list.append(
self._db_api.create_mapping(
value='m1.large',
cost='13.37',
map_type='rate',
field_id=field_db.field_id,
group_id=group_db.group_id))
mappings_uuid = [mapping.mapping_id for mapping in mapping_list]
result = self._hash._load_mappings(mappings_uuid)
expected_result = {
'_DEFAULT_': {
'm1.tiny': {
'cost': decimal.Decimal('1.337'),
'type': 'flat'}},
'test_group': {
'm1.large': {
'cost': decimal.Decimal('13.37'),
'type': 'rate'}}}
self.assertEqual(expected_result, result)
def test_load_thresholds(self):
threshold_list = []
service_db = self._db_api.create_service('compute')
field_db = self._db_api.create_field(service_db.service_id,
'flavor')
group_db = self._db_api.create_group('test_group')
threshold_list.append(
self._db_api.create_threshold(
level='1000',
cost='3.1337',
map_type='flat',
field_id=field_db.field_id,
group_id=group_db.group_id))
thresholds_uuid = [threshold.threshold_id
for threshold in threshold_list]
result = self._hash._load_thresholds(thresholds_uuid)
expected_result = {
'test_group': {
1000: {
'cost': decimal.Decimal('3.1337'),
'type': 'flat'}}}
self.assertEqual(expected_result, result)
def test_process_services(self):
service_db = self._db_api.create_service('compute')
group_db = self._db_api.create_group('test_group')
self._db_api.create_mapping(
@ -447,16 +582,22 @@ class HashMapRatingTest(tests.TestCase):
map_type='flat',
service_id=service_db.service_id)
self._hash.reload_config()
actual_data = CK_RESOURCES_DATA[:]
expected_data = CK_RESOURCES_DATA[:]
actual_data = copy.deepcopy(CK_RESOURCES_DATA)
expected_data = copy.deepcopy(CK_RESOURCES_DATA)
for cur_data in actual_data:
cur_usage = cur_data['usage']
for service_name, service_data in cur_usage.items():
for item in service_data:
self._hash._res = {}
self._hash.process_services(service_name, item)
self._hash.add_rating_informations(item)
compute_list = expected_data[0]['usage']['compute']
compute_list[0]['rating'] = {'price': decimal.Decimal('2.757')}
compute_list[1]['rating'] = {'price': decimal.Decimal('2.757')}
compute_list[1]['rating'] = {'price': decimal.Decimal('5.514')}
compute_list[2]['rating'] = {'price': decimal.Decimal('2.757')}
self._hash.process(actual_data)
self.assertEqual(expected_data, actual_data)
def test_process_field_map(self):
def test_process_fields(self):
service_db = self._db_api.create_service('compute')
flavor_field = self._db_api.create_field(service_db.service_id,
'flavor')
@ -481,20 +622,164 @@ class HashMapRatingTest(tests.TestCase):
map_type='flat',
field_id=flavor_field.field_id)
self._hash.reload_config()
actual_data = CK_RESOURCES_DATA[:]
expected_data = CK_RESOURCES_DATA[:]
actual_data = copy.deepcopy(CK_RESOURCES_DATA)
expected_data = copy.deepcopy(CK_RESOURCES_DATA)
for cur_data in actual_data:
cur_usage = cur_data['usage']
for service_name, service_data in cur_usage.items():
for item in service_data:
self._hash._res = {}
self._hash.process_fields(service_name, item)
self._hash.add_rating_informations(item)
compute_list = expected_data[0]['usage']['compute']
compute_list[0]['rating'] = {'price': decimal.Decimal('1.337')}
compute_list[1]['rating'] = {'price': decimal.Decimal('1.42')}
compute_list[1]['rating'] = {'price': decimal.Decimal('2.84')}
compute_list[2]['rating'] = {'price': decimal.Decimal('1.47070')}
self._hash.process(actual_data)
self.assertEqual(expected_data, actual_data)
def test_process_field_threshold(self):
service_db = self._db_api.create_service('compute')
field_db = self._db_api.create_field(service_db.service_id,
'memory')
self._db_api.create_threshold(
level=64,
cost='0.1337',
map_type='flat',
field_id=field_db.field_id)
self._db_api.create_threshold(
level=128,
cost='0.2',
map_type='flat',
field_id=field_db.field_id)
self._hash.reload_config()
actual_data = copy.deepcopy(CK_RESOURCES_DATA)
expected_data = copy.deepcopy(CK_RESOURCES_DATA)
for cur_data in actual_data:
cur_usage = cur_data['usage']
for service_name, service_data in cur_usage.items():
for item in service_data:
self._hash._res = {}
self._hash.process_fields(service_name, item)
self._hash.add_rating_informations(item)
compute_list = expected_data[0]['usage']['compute']
compute_list[0]['rating'] = {'price': decimal.Decimal('0.1337')}
compute_list[1]['rating'] = {'price': decimal.Decimal('0.4')}
compute_list[2]['rating'] = {'price': decimal.Decimal('0.1337')}
self.assertEqual(expected_data, actual_data)
def test_process_service_threshold(self):
service_db = self._db_api.create_service('compute')
self._db_api.create_threshold(
level=1,
cost='0.1',
map_type='flat',
service_id=service_db.service_id)
self._db_api.create_threshold(
level=2,
cost='0.15',
map_type='flat',
service_id=service_db.service_id)
self._hash.reload_config()
actual_data = copy.deepcopy(CK_RESOURCES_DATA)
expected_data = copy.deepcopy(CK_RESOURCES_DATA)
for cur_data in actual_data:
cur_usage = cur_data['usage']
for service_name, service_data in cur_usage.items():
for item in service_data:
self._hash._res = {}
self._hash.process_services(service_name, item)
self._hash.add_rating_informations(item)
compute_list = expected_data[0]['usage']['compute']
compute_list[0]['rating'] = {'price': decimal.Decimal('0.1')}
compute_list[1]['rating'] = {'price': decimal.Decimal('0.15')}
compute_list[2]['rating'] = {'price': decimal.Decimal('0.1')}
self.assertEqual(expected_data, actual_data)
def test_update_result_flat(self):
self._hash.update_result(
'test_group',
'flat',
1)
self.assertEqual(1, self._hash._res['test_group']['flat'])
self._hash.update_result(
'test_group',
'flat',
0.5)
self.assertEqual(1, self._hash._res['test_group']['flat'])
self._hash.update_result(
'test_group',
'flat',
1.5)
self.assertEqual(1.5, self._hash._res['test_group']['flat'])
def test_update_result_rate(self):
self._hash.update_result(
'test_group',
'rate',
0.5)
self.assertEqual(0.5, self._hash._res['test_group']['rate'])
self._hash.update_result(
'test_group',
'rate',
0.5)
self.assertEqual(0.25, self._hash._res['test_group']['rate'])
self._hash.update_result(
'test_group',
'rate',
1)
self.assertEqual(0.25, self._hash._res['test_group']['rate'])
def test_update_result_threshold(self):
self._hash.update_result(
'test_group',
'flat',
0.01,
0,
True)
self.assertEqual({'level': 0,
'cost': 0.01,
'scope': 'field',
'type': 'flat'},
self._hash._res['test_group']['threshold'])
self._hash.update_result(
'test_group',
'flat',
1,
10,
True)
self.assertEqual({'level': 10,
'cost': 1,
'scope': 'field',
'type': 'flat'},
self._hash._res['test_group']['threshold'])
self._hash.update_result(
'test_group',
'flat',
1.1,
15,
True)
self.assertEqual({'level': 15,
'cost': 1.1,
'scope': 'field',
'type': 'flat'},
self._hash._res['test_group']['threshold'])
self._hash.update_result(
'test_group',
'threshold',
2.2,
10,
True)
self.assertEqual({'level': 15,
'cost': 1.1,
'scope': 'field',
'type': 'flat'},
self._hash._res['test_group']['threshold'])
def test_process_rating(self):
service_db = self._db_api.create_service('compute')
field_db = self._db_api.create_field(service_db.service_id,
flavor_db = self._db_api.create_field(service_db.service_id,
'flavor')
self._db_api.create_group('test_group')
group_db = self._db_api.create_group('test_group')
self._db_api.create_mapping(
cost='1.00',
map_type='flat',
@ -503,18 +788,42 @@ class HashMapRatingTest(tests.TestCase):
value='m1.nano',
cost='1.337',
map_type='flat',
field_id=field_db.field_id)
field_id=flavor_db.field_id,
group_id=group_db.group_id)
self._db_api.create_mapping(
value='m1.tiny',
cost='1.42',
map_type='flat',
field_id=field_db.field_id)
field_id=flavor_db.field_id,
group_id=group_db.group_id)
image_db = self._db_api.create_field(service_db.service_id,
'image_id')
self._db_api.create_mapping(
value='a41fba37-2429-4f15-aa00-b5bc4bf557bf',
cost='1.10',
map_type='rate',
field_id=image_db.field_id,
group_id=group_db.group_id)
memory_db = self._db_api.create_field(service_db.service_id,
'memory')
self._db_api.create_threshold(
level=64,
cost='0.15',
map_type='flat',
field_id=memory_db.field_id,
group_id=group_db.group_id)
self._db_api.create_threshold(
level=128,
cost='0.2',
map_type='flat',
field_id=memory_db.field_id,
group_id=group_db.group_id)
self._hash.reload_config()
actual_data = CK_RESOURCES_DATA[:]
expected_data = CK_RESOURCES_DATA[:]
actual_data = copy.deepcopy(CK_RESOURCES_DATA)
expected_data = copy.deepcopy(CK_RESOURCES_DATA)
compute_list = expected_data[0]['usage']['compute']
compute_list[0]['rating'] = {'price': decimal.Decimal('1.337')}
compute_list[1]['rating'] = {'price': decimal.Decimal('1.42')}
compute_list[2]['rating'] = {'price': decimal.Decimal('1.42')}
compute_list[0]['rating'] = {'price': decimal.Decimal('2.487')}
compute_list[1]['rating'] = {'price': decimal.Decimal('5.564')}
compute_list[2]['rating'] = {'price': decimal.Decimal('2.6357')}
self._hash.process(actual_data)
self.assertEqual(expected_data, actual_data)

View File

@ -32,6 +32,12 @@ HashMap Module REST API
.. autotype:: cloudkitty.rating.hash.datamodels.mapping.MappingCollection
:members:
.. autotype:: cloudkitty.rating.hash.datamodels.threshold.Threshold
:members:
.. autotype:: cloudkitty.rating.hash.datamodels.threshold.ThresholdCollection
:members:
.. rest-controller:: cloudkitty.rating.hash.controllers.group:HashMapGroupsController
:webprefix: /v1/rating/module_config/hashmap/groups