Merge "Added new mapping type: threshold"
This commit is contained in:
commit
f072724991
@ -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
|
||||
|
||||
|
||||
|
@ -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
|
||||
|
||||
|
@ -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,98 +62,203 @@ 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'] *= cost
|
||||
elif map_type == 'flat':
|
||||
new_flat = cost
|
||||
cur_flat = self._res[group]['flat']
|
||||
if new_flat > cur_flat:
|
||||
self._res[group]['flat'] = new_flat
|
||||
|
||||
if map_type == 'rate':
|
||||
self._res[group]['rate'] *= value
|
||||
elif map_type == 'flat':
|
||||
new_flat = value
|
||||
cur_flat = self._res[group]['flat']
|
||||
if new_flat > cur_flat:
|
||||
self._res[group]['flat'] = new_flat
|
||||
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 mapping is mapping_default:
|
||||
continue
|
||||
if cmp_value == mapping_value:
|
||||
self.update_result(
|
||||
group_name,
|
||||
mapping['type'],
|
||||
mapping['cost'])
|
||||
matched = True
|
||||
if not matched and mapping_default:
|
||||
self.update_result(
|
||||
group_name,
|
||||
mapping_default['type'],
|
||||
mapping_default['cost'])
|
||||
|
||||
def process_service_map(self, service_name, data):
|
||||
if service_name not in self._service_mappings:
|
||||
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
|
||||
serv_map = self._service_mappings[service_name]
|
||||
for group_name, mapping in serv_map.items():
|
||||
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_field_map(self, service_name, data):
|
||||
if service_name not in self._field_mappings:
|
||||
return {}
|
||||
field_map = self._field_mappings[service_name]
|
||||
def process_fields(self, service_name, data):
|
||||
if service_name not in self._entries:
|
||||
return
|
||||
desc_data = data['desc']
|
||||
for field_name, group_mappings in field_map.items():
|
||||
field_mappings = self._entries[service_name]['fields']
|
||||
for field_name, group_mappings in field_mappings.items():
|
||||
if field_name not in desc_data:
|
||||
continue
|
||||
for group_name, mappings in group_mappings.items():
|
||||
mapping_default = mappings.pop('_DEFAULT_', {})
|
||||
matched = False
|
||||
for mapping_value, mapping in mappings.items():
|
||||
if desc_data[field_name] == mapping_value:
|
||||
self.update_result(
|
||||
group_name,
|
||||
mapping['type'],
|
||||
mapping['cost'])
|
||||
matched = True
|
||||
if not matched and mapping_default:
|
||||
self.update_result(
|
||||
group_name,
|
||||
mapping_default['type'],
|
||||
mapping_default['cost'])
|
||||
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:
|
||||
@ -160,7 +266,7 @@ class HashMap(rating.RatingProcessorBase):
|
||||
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
|
||||
|
@ -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):
|
||||
|
163
cloudkitty/rating/hash/controllers/threshold.py
Normal file
163
cloudkitty/rating/hash/controllers/threshold.py
Normal 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))
|
@ -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')
|
||||
|
83
cloudkitty/rating/hash/datamodels/threshold.py
Normal file
83
cloudkitty/rating/hash/datamodels/threshold.py
Normal 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])
|
@ -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.
|
||||
"""
|
||||
|
@ -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')
|
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -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,51 +406,170 @@ class HashMapRatingTest(tests.TestCase):
|
||||
mappings = self._db_api.list_mappings(field_uuid=field_db.field_id)
|
||||
self.assertEqual([], mappings)
|
||||
|
||||
# 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))
|
||||
mapping_list.append(
|
||||
self._db_api.create_mapping(
|
||||
value='m1.tiny',
|
||||
cost='1.337',
|
||||
map_type='flat',
|
||||
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=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])
|
||||
|
||||
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'),
|
||||
'type': 'flat'}},
|
||||
'test_group': {
|
||||
'm1.large': {
|
||||
'cost': decimal.Decimal('13.37'),
|
||||
'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_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')
|
||||
self._db_api.create_mapping(
|
||||
cost='1.42',
|
||||
map_type='rate',
|
||||
service_id=service_db.service_id)
|
||||
self._db_api.create_mapping(
|
||||
value='m1.tiny',
|
||||
cost='1.337',
|
||||
map_type='flat',
|
||||
field_id=field_db.field_id)
|
||||
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'),
|
||||
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)
|
||||
|
||||
field_expect = {
|
||||
'compute': {
|
||||
'flavor': {
|
||||
'_DEFAULT_': {
|
||||
'm1.tiny': {
|
||||
'cost': decimal.Decimal('1.337'),
|
||||
'type': 'flat'}},
|
||||
'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)
|
||||
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_service_map(self):
|
||||
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')
|
||||
self._db_api.create_group('test_group')
|
||||
flavor_db = self._db_api.create_field(service_db.service_id,
|
||||
'flavor')
|
||||
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)
|
||||
|
@ -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
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user