New HashMap rating module version
This commit is breaking the old HashMap database models and configuration, migrations can't be applied. - Renamed BasicHashMap to HashMap. - Refactored all the API. - Changed the way configuration is handled: - Every model is now accessed via UUID. - Every model can be seen as "flat" from the new API. - Added calculation grouping, you can now apply multiple HashMap calculations for different metrics, the sum of every group is then applied. - Added two levels of mapping, directly on the service name and on metadata field. - Refactored HashMap to handle the new core API. - Fixed problems with SQL and Numeric truncating data. - Optimized SQL requests and models to use joins and filters. - Added unit tests for the hashmap module. Change-Id: Ibb63b0ac88eb92bec42497e0d72bf9800ea7379c
This commit is contained in:
parent
8ee7535cae
commit
2db226974d
|
@ -15,11 +15,25 @@
|
||||||
#
|
#
|
||||||
# @author: Stéphane Albert
|
# @author: Stéphane Albert
|
||||||
#
|
#
|
||||||
|
from oslo.utils import uuidutils
|
||||||
|
from wsme import exc
|
||||||
from wsme import types as wtypes
|
from wsme import types as wtypes
|
||||||
|
|
||||||
from cloudkitty.i18n import _LE
|
from cloudkitty.i18n import _LE
|
||||||
|
|
||||||
|
|
||||||
|
class UuidType(wtypes.UuidType):
|
||||||
|
"""A simple UUID type."""
|
||||||
|
basetype = wtypes.text
|
||||||
|
name = 'uuid'
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def validate(value):
|
||||||
|
if not uuidutils.is_uuid_like(value):
|
||||||
|
raise exc.InvalidType(_LE("Invalid UUID, got '%s'") % value)
|
||||||
|
return value
|
||||||
|
|
||||||
|
|
||||||
# Code taken from ironic types
|
# Code taken from ironic types
|
||||||
class MultiType(wtypes.UserType):
|
class MultiType(wtypes.UserType):
|
||||||
"""A complex type that represents one or more types.
|
"""A complex type that represents one or more types.
|
||||||
|
|
|
@ -15,210 +15,34 @@
|
||||||
#
|
#
|
||||||
# @author: Stéphane Albert
|
# @author: Stéphane Albert
|
||||||
#
|
#
|
||||||
import pecan
|
|
||||||
from pecan import rest
|
|
||||||
from pecan import routing
|
|
||||||
from wsme import types as wtypes
|
|
||||||
import wsmeext.pecan as wsme_pecan
|
|
||||||
|
|
||||||
from cloudkitty import billing
|
from cloudkitty import billing
|
||||||
from cloudkitty.billing.hash.db import api
|
from cloudkitty.billing.hash.controllers import root as root_api
|
||||||
from cloudkitty.db import api as db_api
|
from cloudkitty.billing.hash.db import api as hash_db_api
|
||||||
|
from cloudkitty.db import api as ck_db_api
|
||||||
from cloudkitty.openstack.common import log as logging
|
from cloudkitty.openstack.common import log as logging
|
||||||
|
|
||||||
LOG = logging.getLogger(__name__)
|
LOG = logging.getLogger(__name__)
|
||||||
|
|
||||||
MAP_TYPE = wtypes.Enum(wtypes.text, 'flat', 'rate')
|
|
||||||
|
|
||||||
|
class HashMap(billing.BillingProcessorBase):
|
||||||
|
"""HashMap rating module.
|
||||||
|
|
||||||
class Mapping(wtypes.Base):
|
HashMap can be used to map arbitrary fields of a resource to different
|
||||||
|
costs.
|
||||||
map_type = wtypes.wsattr(MAP_TYPE, default='rate', name='type')
|
|
||||||
"""Type of the mapping."""
|
|
||||||
|
|
||||||
value = wtypes.wsattr(float, mandatory=True)
|
|
||||||
"""Value of the mapping."""
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def sample(cls):
|
|
||||||
sample = cls(value=4.2)
|
|
||||||
return sample
|
|
||||||
|
|
||||||
|
|
||||||
class BasicHashMapConfigController(rest.RestController):
|
|
||||||
"""RestController for hashmap's configuration."""
|
|
||||||
|
|
||||||
_custom_actions = {
|
|
||||||
'types': ['GET']
|
|
||||||
}
|
|
||||||
|
|
||||||
@wsme_pecan.wsexpose([wtypes.text])
|
|
||||||
def get_types(self):
|
|
||||||
"""Return the list of every mapping type available.
|
|
||||||
|
|
||||||
"""
|
"""
|
||||||
return MAP_TYPE.values
|
|
||||||
|
|
||||||
@pecan.expose()
|
|
||||||
def _route(self, args, request=None):
|
|
||||||
if len(args) > 2:
|
|
||||||
# Taken from base _route function
|
|
||||||
if request is None:
|
|
||||||
from pecan import request # noqa
|
|
||||||
method = request.params.get('_method', request.method).lower()
|
|
||||||
if request.method == 'GET' and method in ('delete', 'put'):
|
|
||||||
pecan.abort(405)
|
|
||||||
|
|
||||||
if request.method == 'GET':
|
|
||||||
return routing.lookup_controller(self.get_mapping, args)
|
|
||||||
return super(BasicHashMapConfigController, self)._route(args)
|
|
||||||
|
|
||||||
@wsme_pecan.wsexpose(Mapping, wtypes.text, wtypes.text, wtypes.text)
|
|
||||||
def get_mapping(self, service, field, key):
|
|
||||||
"""Get a mapping from full path.
|
|
||||||
|
|
||||||
"""
|
|
||||||
hashmap = api.get_instance()
|
|
||||||
try:
|
|
||||||
return hashmap.get_mapping(service, field, key)
|
|
||||||
except (api.NoSuchService, api.NoSuchField, api.NoSuchMapping) as e:
|
|
||||||
pecan.abort(400, str(e))
|
|
||||||
|
|
||||||
@wsme_pecan.wsexpose([wtypes.text])
|
|
||||||
def get(self):
|
|
||||||
"""Get the service list
|
|
||||||
|
|
||||||
:return: List of every services' name.
|
|
||||||
"""
|
|
||||||
hashmap = api.get_instance()
|
|
||||||
return [service.name for service in hashmap.list_services()]
|
|
||||||
|
|
||||||
@wsme_pecan.wsexpose([wtypes.text], wtypes.text, wtypes.text)
|
|
||||||
def get_one(self, service=None, field=None):
|
|
||||||
"""Return the list of every sub keys.
|
|
||||||
|
|
||||||
:param service: (Optional) Filter on this service.
|
|
||||||
:param field: (Optional) Filter on this field.
|
|
||||||
"""
|
|
||||||
hashmap = api.get_instance()
|
|
||||||
if field:
|
|
||||||
try:
|
|
||||||
return [mapping.key for mapping in hashmap.list_mappings(
|
|
||||||
service,
|
|
||||||
field)]
|
|
||||||
except (api.NoSuchService, api.NoSuchField) as e:
|
|
||||||
pecan.abort(400, str(e))
|
|
||||||
|
|
||||||
else:
|
|
||||||
try:
|
|
||||||
return [f.name for f in hashmap.list_fields(service)]
|
|
||||||
except api.NoSuchService as e:
|
|
||||||
pecan.abort(400, str(e))
|
|
||||||
|
|
||||||
# FIXME (sheeprine): Still a problem with our routing and the different
|
|
||||||
# object types. For service/field it's text or a mapping.
|
|
||||||
@wsme_pecan.wsexpose(None, wtypes.text, wtypes.text, wtypes.text,
|
|
||||||
body=Mapping)
|
|
||||||
def post(self, service, field=None, key=None, mapping=None):
|
|
||||||
"""Create hashmap fields.
|
|
||||||
|
|
||||||
:param service: Name of the service to create.
|
|
||||||
:param field: (Optional) Name of the field to create.
|
|
||||||
:param key: (Optional) Name of the key to create.
|
|
||||||
:param mapping: (Optional) Mapping object to create.
|
|
||||||
"""
|
|
||||||
hashmap = api.get_instance()
|
|
||||||
if field:
|
|
||||||
if key:
|
|
||||||
if mapping:
|
|
||||||
try:
|
|
||||||
# FIXME(sheeprine): We should return the result
|
|
||||||
hashmap.create_mapping(
|
|
||||||
service,
|
|
||||||
field,
|
|
||||||
key,
|
|
||||||
value=mapping.value,
|
|
||||||
map_type=mapping.map_type
|
|
||||||
)
|
|
||||||
pecan.response.headers['Location'] = pecan.request.path
|
|
||||||
except api.MappingAlreadyExists as e:
|
|
||||||
pecan.abort(409, str(e))
|
|
||||||
else:
|
|
||||||
e = ValueError('Mapping can\'t be empty.')
|
|
||||||
pecan.abort(400, str(e))
|
|
||||||
else:
|
|
||||||
try:
|
|
||||||
hashmap.create_field(service, field)
|
|
||||||
pecan.response.headers['Location'] = pecan.request.path
|
|
||||||
except api.FieldAlreadyExists as e:
|
|
||||||
pecan.abort(409, str(e))
|
|
||||||
else:
|
|
||||||
try:
|
|
||||||
hashmap.create_service(service)
|
|
||||||
pecan.response.headers['Location'] = pecan.request.path
|
|
||||||
except api.ServiceAlreadyExists as e:
|
|
||||||
pecan.abort(409, str(e))
|
|
||||||
self.notify_reload()
|
|
||||||
pecan.response.status = 201
|
|
||||||
|
|
||||||
@wsme_pecan.wsexpose(None, wtypes.text, wtypes.text, wtypes.text,
|
|
||||||
body=Mapping)
|
|
||||||
def put(self, service, field, key, mapping):
|
|
||||||
"""Modify hashmap fields
|
|
||||||
|
|
||||||
:param service: Filter on this service.
|
|
||||||
:param field: Filter on this field.
|
|
||||||
:param key: Modify the content of this key.
|
|
||||||
:param mapping: Mapping object to update.
|
|
||||||
"""
|
|
||||||
hashmap = api.get_instance()
|
|
||||||
try:
|
|
||||||
hashmap.update_mapping(
|
|
||||||
service,
|
|
||||||
field,
|
|
||||||
key,
|
|
||||||
value=mapping.value,
|
|
||||||
map_type=mapping.map_type
|
|
||||||
)
|
|
||||||
pecan.response.headers['Location'] = pecan.request.path
|
|
||||||
pecan.response.status = 204
|
|
||||||
except (api.NoSuchService, api.NoSuchField, api.NoSuchMapping) as e:
|
|
||||||
pecan.abort(400, str(e))
|
|
||||||
|
|
||||||
@wsme_pecan.wsexpose(None, wtypes.text, wtypes.text, wtypes.text)
|
|
||||||
def delete(self, service, field=None, key=None):
|
|
||||||
"""Delete the parent and all the sub keys recursively.
|
|
||||||
|
|
||||||
:param service: Name of the service to delete.
|
|
||||||
:param field: (Optional) Name of the field to delete.
|
|
||||||
:param key: (Optional) Name of the key to delete.
|
|
||||||
"""
|
|
||||||
hashmap = api.get_instance()
|
|
||||||
try:
|
|
||||||
if field:
|
|
||||||
if key:
|
|
||||||
hashmap.delete_mapping(service, field, key)
|
|
||||||
else:
|
|
||||||
hashmap.delete_field(service, field)
|
|
||||||
else:
|
|
||||||
hashmap.delete_service(service)
|
|
||||||
except (api.NoSuchService, api.NoSuchField, api.NoSuchMapping) as e:
|
|
||||||
pecan.abort(400, str(e))
|
|
||||||
pecan.response.status = 204
|
|
||||||
|
|
||||||
|
|
||||||
class BasicHashMap(billing.BillingProcessorBase):
|
|
||||||
|
|
||||||
module_name = 'hashmap'
|
module_name = 'hashmap'
|
||||||
description = 'Basic hashmap billing module.'
|
description = 'Basic hashmap billing module.'
|
||||||
hot_config = True
|
hot_config = True
|
||||||
config_controller = BasicHashMapConfigController
|
config_controller = root_api.HashMapConfigController
|
||||||
|
|
||||||
db_api = api.get_instance()
|
db_api = hash_db_api.get_instance()
|
||||||
|
|
||||||
def __init__(self, tenant_id=None):
|
def __init__(self, tenant_id=None):
|
||||||
super(BasicHashMap, self).__init__(tenant_id)
|
super(HashMap, self).__init__(tenant_id)
|
||||||
self._billing_info = {}
|
self._service_mappings = {}
|
||||||
|
self._field_mappings = {}
|
||||||
|
self._res = {}
|
||||||
self._load_billing_rates()
|
self._load_billing_rates()
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
@ -227,67 +51,127 @@ class BasicHashMap(billing.BillingProcessorBase):
|
||||||
|
|
||||||
:returns: bool if module is enabled
|
:returns: bool if module is enabled
|
||||||
"""
|
"""
|
||||||
# FIXME(sheeprine): Hardcoded values to check the state
|
db_api = ck_db_api.get_instance()
|
||||||
api = db_api.get_instance()
|
module_db = db_api.get_module_enable_state()
|
||||||
module_db = api.get_module_enable_state()
|
|
||||||
return module_db.get_state('hashmap') or False
|
return module_db.get_state('hashmap') or False
|
||||||
|
|
||||||
def reload_config(self):
|
def reload_config(self):
|
||||||
|
"""Reload the module's configuration.
|
||||||
|
|
||||||
|
"""
|
||||||
self._load_billing_rates()
|
self._load_billing_rates()
|
||||||
|
|
||||||
def _load_billing_rates(self):
|
def _load_mappings(self, mappings_uuid_list):
|
||||||
self._billing_info = {}
|
hashmap = hash_db_api.get_instance()
|
||||||
hashmap = api.get_instance()
|
mappings = {}
|
||||||
services = hashmap.list_services()
|
for mapping_uuid in mappings_uuid_list:
|
||||||
for service in services:
|
mapping_db = hashmap.get_mapping(uuid=mapping_uuid)
|
||||||
service = service[0]
|
if mapping_db.group_id:
|
||||||
self._billing_info[service] = {}
|
group_name = mapping_db.group.name
|
||||||
fields = hashmap.list_fields(service)
|
else:
|
||||||
for field in fields:
|
group_name = '_DEFAULT_'
|
||||||
field = field[0]
|
if group_name not in mappings:
|
||||||
self._billing_info[service][field] = {}
|
mappings[group_name] = {}
|
||||||
mappings = hashmap.list_mappings(service, field)
|
mapping_value = mapping_db.value
|
||||||
for mapping in mappings:
|
|
||||||
mapping = mapping[0]
|
|
||||||
mapping_db = hashmap.get_mapping(service, field, mapping)
|
|
||||||
map_dict = {}
|
map_dict = {}
|
||||||
map_dict['value'] = mapping_db.value
|
map_dict['cost'] = mapping_db.cost
|
||||||
map_dict['type'] = mapping_db.map_type
|
map_dict['type'] = mapping_db.map_type
|
||||||
self._billing_info[service][field][mapping] = map_dict
|
if mapping_value:
|
||||||
|
mappings[group_name][mapping_value] = map_dict
|
||||||
|
else:
|
||||||
|
mappings[group_name] = map_dict
|
||||||
|
return mappings
|
||||||
|
|
||||||
def process_service(self, name, data):
|
def _load_service_mappings(self, service_name, service_uuid):
|
||||||
if name not in self._billing_info:
|
hashmap = hash_db_api.get_instance()
|
||||||
return
|
mappings_uuid_list = hashmap.list_mappings(service_uuid=service_uuid)
|
||||||
serv_b_info = self._billing_info[name]
|
mappings = self._load_mappings(mappings_uuid_list)
|
||||||
for entry in data:
|
if mappings:
|
||||||
flat = 0
|
self._service_mappings[service_name] = mappings
|
||||||
rate = 1
|
|
||||||
entry_desc = entry['desc']
|
|
||||||
for field in serv_b_info:
|
|
||||||
if field not in entry_desc:
|
|
||||||
continue
|
|
||||||
b_info = serv_b_info[field]
|
|
||||||
key = entry_desc[field]
|
|
||||||
|
|
||||||
value = 0
|
def _load_field_mappings(self, service_name, field_name, field_uuid):
|
||||||
if key in b_info:
|
hashmap = hash_db_api.get_instance()
|
||||||
value = b_info[key]['value']
|
mappings_uuid_list = hashmap.list_mappings(field_uuid=field_uuid)
|
||||||
elif '_DEFAULT_' in b_info:
|
mappings = self._load_mappings(mappings_uuid_list)
|
||||||
value = b_info['_DEFAULT_']
|
if mappings:
|
||||||
|
self._field_mappings[service_name] = {}
|
||||||
|
self._field_mappings[service_name][field_name] = mappings
|
||||||
|
|
||||||
if value:
|
def _load_billing_rates(self):
|
||||||
if b_info[key]['type'] == 'rate':
|
self._service_mappings = {}
|
||||||
rate *= value
|
self._field_mappings = {}
|
||||||
elif b_info[key]['type'] == 'flat':
|
hashmap = hash_db_api.get_instance()
|
||||||
new_flat = 0
|
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)
|
||||||
|
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)
|
||||||
|
|
||||||
|
def add_billing_informations(self, data):
|
||||||
|
if 'billing' not in data:
|
||||||
|
data['billing'] = {'price': 0}
|
||||||
|
for entry in self._res.values():
|
||||||
|
res = entry['rate'] * entry['flat']
|
||||||
|
data['billing']['price'] += res * data['vol']['qty']
|
||||||
|
|
||||||
|
def update_result(self, group, map_type, value):
|
||||||
|
if group not in self._res:
|
||||||
|
self._res[group] = {'flat': 0,
|
||||||
|
'rate': 1}
|
||||||
|
|
||||||
|
if map_type == 'rate':
|
||||||
|
self._res[group]['rate'] *= value
|
||||||
|
elif map_type == 'flat':
|
||||||
new_flat = value
|
new_flat = value
|
||||||
if new_flat > flat:
|
cur_flat = self._res[group]['flat']
|
||||||
flat = new_flat
|
if new_flat > cur_flat:
|
||||||
entry['billing'] = {'price': flat * rate}
|
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_', {})
|
||||||
|
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'])
|
||||||
|
|
||||||
def process(self, data):
|
def process(self, data):
|
||||||
for cur_data in data:
|
for cur_data in data:
|
||||||
cur_usage = cur_data['usage']
|
cur_usage = cur_data['usage']
|
||||||
for service in cur_usage:
|
for service_name, service_data in cur_usage.items():
|
||||||
self.process_service(service, cur_usage[service])
|
for item in service_data:
|
||||||
|
self._res = {}
|
||||||
|
self.process_service_map(service_name, item)
|
||||||
|
self.process_field_map(service_name, item)
|
||||||
|
self.add_billing_informations(item)
|
||||||
return data
|
return data
|
||||||
|
|
|
@ -0,0 +1,100 @@
|
||||||
|
# -*- 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.billing.hash.datamodels import field as field_models
|
||||||
|
from cloudkitty.billing.hash.db import api as db_api
|
||||||
|
|
||||||
|
|
||||||
|
class HashMapFieldsController(rest.RestController):
|
||||||
|
"""Controller responsible of fields management.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
@wsme_pecan.wsexpose(field_models.FieldCollection,
|
||||||
|
ck_types.UuidType(),
|
||||||
|
status_code=200)
|
||||||
|
def get_all(self, service_id):
|
||||||
|
"""Get the field list.
|
||||||
|
|
||||||
|
:param service_id: Service's UUID to filter on.
|
||||||
|
:return: List of every fields.
|
||||||
|
"""
|
||||||
|
hashmap = db_api.get_instance()
|
||||||
|
field_list = []
|
||||||
|
fields_uuid_list = hashmap.list_fields(service_id)
|
||||||
|
for field_uuid in fields_uuid_list:
|
||||||
|
field_db = hashmap.get_field(field_uuid)
|
||||||
|
field_list.append(field_models.Field(
|
||||||
|
**field_db.export_model()))
|
||||||
|
res = field_models.FieldCollection(fields=field_list)
|
||||||
|
return res
|
||||||
|
|
||||||
|
@wsme_pecan.wsexpose(field_models.Field,
|
||||||
|
ck_types.UuidType(),
|
||||||
|
status_code=200)
|
||||||
|
def get_one(self, field_id):
|
||||||
|
"""Return a field.
|
||||||
|
|
||||||
|
:param field_id: UUID of the field to filter on.
|
||||||
|
"""
|
||||||
|
hashmap = db_api.get_instance()
|
||||||
|
try:
|
||||||
|
field_db = hashmap.get_field(uuid=field_id)
|
||||||
|
return field_models.Field(**field_db.export_model())
|
||||||
|
except db_api.NoSuchField as e:
|
||||||
|
pecan.abort(400, str(e))
|
||||||
|
|
||||||
|
@wsme_pecan.wsexpose(field_models.Field,
|
||||||
|
body=field_models.Field,
|
||||||
|
status_code=201)
|
||||||
|
def post(self, field_data):
|
||||||
|
"""Create a field.
|
||||||
|
|
||||||
|
:param field_data: Informations about the field to create.
|
||||||
|
"""
|
||||||
|
hashmap = db_api.get_instance()
|
||||||
|
try:
|
||||||
|
field_db = hashmap.create_field(
|
||||||
|
field_data.service_id,
|
||||||
|
field_data.name)
|
||||||
|
pecan.response.location = pecan.request.path_url
|
||||||
|
if pecan.response.location[-1] != '/':
|
||||||
|
pecan.response.location += '/'
|
||||||
|
pecan.response.location += field_db.field_id
|
||||||
|
return field_models.Field(
|
||||||
|
**field_db.export_model())
|
||||||
|
except (db_api.FieldAlreadyExists, db_api.NoSuchService) as e:
|
||||||
|
pecan.abort(409, str(e))
|
||||||
|
|
||||||
|
@wsme_pecan.wsexpose(None,
|
||||||
|
ck_types.UuidType(),
|
||||||
|
status_code=204)
|
||||||
|
def delete(self, field_id):
|
||||||
|
"""Delete the field and all the sub keys recursively.
|
||||||
|
|
||||||
|
:param field_id: UUID of the field to delete.
|
||||||
|
"""
|
||||||
|
hashmap = db_api.get_instance()
|
||||||
|
try:
|
||||||
|
hashmap.delete_field(uuid=field_id)
|
||||||
|
except db_api.NoSuchService as e:
|
||||||
|
pecan.abort(400, str(e))
|
|
@ -0,0 +1,117 @@
|
||||||
|
# -*- 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.billing.hash.datamodels import group as group_models
|
||||||
|
from cloudkitty.billing.hash.datamodels import mapping as mapping_models
|
||||||
|
from cloudkitty.billing.hash.db import api as db_api
|
||||||
|
|
||||||
|
|
||||||
|
class HashMapGroupsController(rest.RestController):
|
||||||
|
"""Controller responsible of groups management.
|
||||||
|
|
||||||
|
"""
|
||||||
|
_custom_actions = {
|
||||||
|
'mappings': ['GET']
|
||||||
|
}
|
||||||
|
|
||||||
|
@wsme_pecan.wsexpose(mapping_models.MappingCollection,
|
||||||
|
ck_types.UuidType())
|
||||||
|
def mappings(self, group_id):
|
||||||
|
"""Get the mappings attached to the group.
|
||||||
|
|
||||||
|
:param group_id: UUID of the group to filter on.
|
||||||
|
"""
|
||||||
|
hashmap = db_api.get_instance()
|
||||||
|
mapping_list = []
|
||||||
|
mappings_uuid_list = hashmap.list_mappings(group_uuid=group_id)
|
||||||
|
for mapping_uuid in mappings_uuid_list:
|
||||||
|
mapping_db = hashmap.get_mapping(uuid=mapping_uuid)
|
||||||
|
mapping_list.append(mapping_models.Mapping(
|
||||||
|
**mapping_db.export_model()))
|
||||||
|
res = mapping_models.MappingCollection(mappings=mapping_list)
|
||||||
|
return res
|
||||||
|
|
||||||
|
@wsme_pecan.wsexpose(group_models.GroupCollection)
|
||||||
|
def get_all(self):
|
||||||
|
"""Get the group list
|
||||||
|
|
||||||
|
:return: List of every group.
|
||||||
|
"""
|
||||||
|
hashmap = db_api.get_instance()
|
||||||
|
group_list = []
|
||||||
|
groups_uuid_list = hashmap.list_groups()
|
||||||
|
for group_uuid in groups_uuid_list:
|
||||||
|
group_db = hashmap.get_group(uuid=group_uuid)
|
||||||
|
group_list.append(group_models.Group(
|
||||||
|
**group_db.export_model()))
|
||||||
|
res = group_models.GroupCollection(groups=group_list)
|
||||||
|
return res
|
||||||
|
|
||||||
|
@wsme_pecan.wsexpose(group_models.Group,
|
||||||
|
ck_types.UuidType())
|
||||||
|
def get_one(self, group_id):
|
||||||
|
"""Return a group.
|
||||||
|
|
||||||
|
:param group_id: UUID of the group to filter on.
|
||||||
|
"""
|
||||||
|
hashmap = db_api.get_instance()
|
||||||
|
try:
|
||||||
|
group_db = hashmap.get_group(uuid=group_id)
|
||||||
|
return group_models.Group(**group_db.export_model())
|
||||||
|
except db_api.NoSuchGroup as e:
|
||||||
|
pecan.abort(400, str(e))
|
||||||
|
|
||||||
|
@wsme_pecan.wsexpose(group_models.Group,
|
||||||
|
body=group_models.Group,
|
||||||
|
status_code=201)
|
||||||
|
def post(self, group_data):
|
||||||
|
"""Create a group.
|
||||||
|
|
||||||
|
:param group_data: Informations about the group to create.
|
||||||
|
"""
|
||||||
|
hashmap = db_api.get_instance()
|
||||||
|
try:
|
||||||
|
group_db = hashmap.create_group(group_data.name)
|
||||||
|
pecan.response.location = pecan.request.path_url
|
||||||
|
if pecan.response.location[-1] != '/':
|
||||||
|
pecan.response.location += '/'
|
||||||
|
pecan.response.location += group_db.group_id
|
||||||
|
return group_models.Group(
|
||||||
|
**group_db.export_model())
|
||||||
|
except db_api.GroupAlreadyExists as e:
|
||||||
|
pecan.abort(409, str(e))
|
||||||
|
|
||||||
|
@wsme_pecan.wsexpose(None,
|
||||||
|
ck_types.UuidType(),
|
||||||
|
bool,
|
||||||
|
status_code=204)
|
||||||
|
def delete(self, group_id, recursive=False):
|
||||||
|
"""Delete a group.
|
||||||
|
|
||||||
|
:param group_id: UUID of the group to delete.
|
||||||
|
:param recursive: Delete mappings recursively.
|
||||||
|
"""
|
||||||
|
hashmap = db_api.get_instance()
|
||||||
|
try:
|
||||||
|
hashmap.delete_group(uuid=group_id, recurse=recursive)
|
||||||
|
except db_api.NoSuchGroup as e:
|
||||||
|
pecan.abort(400, str(e))
|
|
@ -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.billing.hash.datamodels import group as group_models
|
||||||
|
from cloudkitty.billing.hash.datamodels import mapping as mapping_models
|
||||||
|
from cloudkitty.billing.hash.db import api as db_api
|
||||||
|
|
||||||
|
|
||||||
|
class HashMapMappingsController(rest.RestController):
|
||||||
|
"""Controller responsible of mappings management.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
_custom_actions = {
|
||||||
|
'group': ['GET']
|
||||||
|
}
|
||||||
|
|
||||||
|
@wsme_pecan.wsexpose(group_models.Group,
|
||||||
|
ck_types.UuidType())
|
||||||
|
def group(self, mapping_id):
|
||||||
|
"""Get the group attached to the mapping.
|
||||||
|
|
||||||
|
:param mapping_id: UUID of the mapping to filter on.
|
||||||
|
"""
|
||||||
|
hashmap = db_api.get_instance()
|
||||||
|
try:
|
||||||
|
group_db = hashmap.get_group_from_mapping(
|
||||||
|
uuid=mapping_id)
|
||||||
|
return group_models.Group(**group_db.export_model())
|
||||||
|
except db_api.MappingHasNoGroup as e:
|
||||||
|
pecan.abort(404, str(e))
|
||||||
|
|
||||||
|
@wsme_pecan.wsexpose(mapping_models.MappingCollection,
|
||||||
|
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 mapping 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 mappings.
|
||||||
|
:return: List of every mappings.
|
||||||
|
"""
|
||||||
|
hashmap = db_api.get_instance()
|
||||||
|
mapping_list = []
|
||||||
|
mappings_uuid_list = hashmap.list_mappings(service_uuid=service_id,
|
||||||
|
field_uuid=field_id,
|
||||||
|
group_uuid=group_id)
|
||||||
|
for mapping_uuid in mappings_uuid_list:
|
||||||
|
mapping_db = hashmap.get_mapping(uuid=mapping_uuid)
|
||||||
|
mapping_list.append(mapping_models.Mapping(
|
||||||
|
**mapping_db.export_model()))
|
||||||
|
res = mapping_models.MappingCollection(mappings=mapping_list)
|
||||||
|
return res
|
||||||
|
|
||||||
|
@wsme_pecan.wsexpose(mapping_models.Mapping,
|
||||||
|
ck_types.UuidType())
|
||||||
|
def get_one(self, mapping_id):
|
||||||
|
"""Return a mapping.
|
||||||
|
|
||||||
|
:param mapping_id: UUID of the mapping to filter on.
|
||||||
|
"""
|
||||||
|
hashmap = db_api.get_instance()
|
||||||
|
try:
|
||||||
|
mapping_db = hashmap.get_mapping(uuid=mapping_id)
|
||||||
|
return mapping_models.Mapping(
|
||||||
|
**mapping_db.export_model())
|
||||||
|
except db_api.NoSuchMapping as e:
|
||||||
|
pecan.abort(400, str(e))
|
||||||
|
|
||||||
|
@wsme_pecan.wsexpose(mapping_models.Mapping,
|
||||||
|
body=mapping_models.Mapping,
|
||||||
|
status_code=201)
|
||||||
|
def post(self, mapping_data):
|
||||||
|
"""Create a mapping.
|
||||||
|
|
||||||
|
:param mapping_data: Informations about the mapping to create.
|
||||||
|
"""
|
||||||
|
hashmap = db_api.get_instance()
|
||||||
|
try:
|
||||||
|
mapping_db = hashmap.create_mapping(
|
||||||
|
value=mapping_data.value,
|
||||||
|
map_type=mapping_data.map_type,
|
||||||
|
cost=mapping_data.cost,
|
||||||
|
field_id=mapping_data.field_id,
|
||||||
|
group_id=mapping_data.group_id,
|
||||||
|
service_id=mapping_data.service_id)
|
||||||
|
pecan.response.location = pecan.request.path_url
|
||||||
|
if pecan.response.location[-1] != '/':
|
||||||
|
pecan.response.location += '/'
|
||||||
|
pecan.response.location += mapping_db.mapping_id
|
||||||
|
return mapping_models.Mapping(
|
||||||
|
**mapping_db.export_model())
|
||||||
|
except db_api.MappingAlreadyExists as e:
|
||||||
|
pecan.abort(409, str(e))
|
||||||
|
|
||||||
|
@wsme_pecan.wsexpose(None,
|
||||||
|
ck_types.UuidType(),
|
||||||
|
body=mapping_models.Mapping,
|
||||||
|
status_code=302)
|
||||||
|
def put(self, mapping_id, mapping):
|
||||||
|
"""Update a mapping.
|
||||||
|
|
||||||
|
:param mapping_id: UUID of the mapping to update.
|
||||||
|
:param mapping: Mapping data to insert.
|
||||||
|
"""
|
||||||
|
hashmap = db_api.get_instance()
|
||||||
|
try:
|
||||||
|
hashmap.update_mapping(
|
||||||
|
mapping_id,
|
||||||
|
mapping_id=mapping.mapping_id,
|
||||||
|
value=mapping.value,
|
||||||
|
cost=mapping.cost,
|
||||||
|
map_type=mapping.map_type,
|
||||||
|
group_id=mapping.group_id)
|
||||||
|
pecan.response.headers['Location'] = pecan.request.path
|
||||||
|
except (db_api.NoSuchService,
|
||||||
|
db_api.NoSuchField,
|
||||||
|
db_api.NoSuchMapping) as e:
|
||||||
|
pecan.abort(400, str(e))
|
||||||
|
|
||||||
|
@wsme_pecan.wsexpose(None,
|
||||||
|
ck_types.UuidType(),
|
||||||
|
status_code=204)
|
||||||
|
def delete(self, mapping_id):
|
||||||
|
"""Delete a mapping.
|
||||||
|
|
||||||
|
:param mapping_id: UUID of the mapping to delete.
|
||||||
|
"""
|
||||||
|
hashmap = db_api.get_instance()
|
||||||
|
try:
|
||||||
|
hashmap.delete_mapping(uuid=mapping_id)
|
||||||
|
except (db_api.NoSuchService,
|
||||||
|
db_api.NoSuchField,
|
||||||
|
db_api.NoSuchMapping) as e:
|
||||||
|
pecan.abort(400, str(e))
|
|
@ -0,0 +1,48 @@
|
||||||
|
# -*- 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
|
||||||
|
#
|
||||||
|
from pecan import rest
|
||||||
|
from wsme import types as wtypes
|
||||||
|
import wsmeext.pecan as wsme_pecan
|
||||||
|
|
||||||
|
from cloudkitty.billing.hash.controllers import field as field_api
|
||||||
|
from cloudkitty.billing.hash.controllers import group as group_api
|
||||||
|
from cloudkitty.billing.hash.controllers import mapping as mapping_api
|
||||||
|
from cloudkitty.billing.hash.controllers import service as service_api
|
||||||
|
from cloudkitty.billing.hash.datamodels import mapping as mapping_models
|
||||||
|
|
||||||
|
|
||||||
|
class HashMapConfigController(rest.RestController):
|
||||||
|
"""Controller exposing all management sub controllers.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
_custom_actions = {
|
||||||
|
'types': ['GET']
|
||||||
|
}
|
||||||
|
|
||||||
|
services = service_api.HashMapServicesController()
|
||||||
|
fields = field_api.HashMapFieldsController()
|
||||||
|
groups = group_api.HashMapGroupsController()
|
||||||
|
mappings = mapping_api.HashMapMappingsController()
|
||||||
|
|
||||||
|
@wsme_pecan.wsexpose([wtypes.text])
|
||||||
|
def get_types(self):
|
||||||
|
"""Return the list of every mapping type available.
|
||||||
|
|
||||||
|
"""
|
||||||
|
return mapping_models.MAP_TYPE.values
|
|
@ -0,0 +1,94 @@
|
||||||
|
# -*- 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.billing.hash.controllers import field as field_api
|
||||||
|
from cloudkitty.billing.hash.datamodels import service as service_models
|
||||||
|
from cloudkitty.billing.hash.db import api as db_api
|
||||||
|
|
||||||
|
|
||||||
|
class HashMapServicesController(rest.RestController):
|
||||||
|
"""Controller responsible of services management.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
fields = field_api.HashMapFieldsController()
|
||||||
|
|
||||||
|
@wsme_pecan.wsexpose(service_models.ServiceCollection)
|
||||||
|
def get_all(self):
|
||||||
|
"""Get the service list
|
||||||
|
|
||||||
|
:return: List of every services.
|
||||||
|
"""
|
||||||
|
hashmap = db_api.get_instance()
|
||||||
|
service_list = []
|
||||||
|
services_uuid_list = hashmap.list_services()
|
||||||
|
for service_uuid in services_uuid_list:
|
||||||
|
service_db = hashmap.get_service(uuid=service_uuid)
|
||||||
|
service_list.append(service_models.Service(
|
||||||
|
**service_db.export_model()))
|
||||||
|
res = service_models.ServiceCollection(services=service_list)
|
||||||
|
return res
|
||||||
|
|
||||||
|
@wsme_pecan.wsexpose(service_models.Service, ck_types.UuidType())
|
||||||
|
def get_one(self, service_id):
|
||||||
|
"""Return a service.
|
||||||
|
|
||||||
|
:param service_id: UUID of the service to filter on.
|
||||||
|
"""
|
||||||
|
hashmap = db_api.get_instance()
|
||||||
|
try:
|
||||||
|
service_db = hashmap.get_service(uuid=service_id)
|
||||||
|
return service_models.Service(**service_db.export_model())
|
||||||
|
except db_api.NoSuchService as e:
|
||||||
|
pecan.abort(400, str(e))
|
||||||
|
|
||||||
|
@wsme_pecan.wsexpose(service_models.Service,
|
||||||
|
body=service_models.Service,
|
||||||
|
status_code=201)
|
||||||
|
def post(self, service_data):
|
||||||
|
"""Create hashmap service.
|
||||||
|
|
||||||
|
:param service_data: Informations about the service to create.
|
||||||
|
"""
|
||||||
|
hashmap = db_api.get_instance()
|
||||||
|
try:
|
||||||
|
service_db = hashmap.create_service(service_data.name)
|
||||||
|
pecan.response.location = pecan.request.path_url
|
||||||
|
if pecan.response.location[-1] != '/':
|
||||||
|
pecan.response.location += '/'
|
||||||
|
pecan.response.location += service_db.service_id
|
||||||
|
return service_models.Service(
|
||||||
|
**service_db.export_model())
|
||||||
|
except db_api.ServiceAlreadyExists as e:
|
||||||
|
pecan.abort(409, str(e))
|
||||||
|
|
||||||
|
@wsme_pecan.wsexpose(None, ck_types.UuidType(), status_code=204)
|
||||||
|
def delete(self, service_id):
|
||||||
|
"""Delete the service and all the sub keys recursively.
|
||||||
|
|
||||||
|
:param service_id: UUID of the service to delete.
|
||||||
|
"""
|
||||||
|
hashmap = db_api.get_instance()
|
||||||
|
try:
|
||||||
|
hashmap.delete_service(uuid=service_id)
|
||||||
|
except db_api.NoSuchService as e:
|
||||||
|
pecan.abort(400, str(e))
|
|
@ -0,0 +1,61 @@
|
||||||
|
# -*- 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
|
||||||
|
#
|
||||||
|
from wsme import types as wtypes
|
||||||
|
|
||||||
|
from cloudkitty.api.v1 import types as ck_types
|
||||||
|
|
||||||
|
|
||||||
|
class Field(wtypes.Base):
|
||||||
|
"""Type describing a field.
|
||||||
|
|
||||||
|
A field is mapping a value of the 'desc' dict of the CloudKitty data. It's
|
||||||
|
used to map the name of a metadata.
|
||||||
|
"""
|
||||||
|
|
||||||
|
field_id = wtypes.wsattr(ck_types.UuidType(),
|
||||||
|
mandatory=False,
|
||||||
|
readonly=True)
|
||||||
|
"""UUID of the field."""
|
||||||
|
|
||||||
|
name = wtypes.wsattr(wtypes.text, mandatory=True)
|
||||||
|
"""Name of the field."""
|
||||||
|
|
||||||
|
service_id = wtypes.wsattr(ck_types.UuidType(),
|
||||||
|
mandatory=True)
|
||||||
|
"""UUID of the parent service."""
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def sample(cls):
|
||||||
|
sample = cls(field_id='ac55b000-a05b-4832-b2ff-265a034886ab',
|
||||||
|
name='image_id',
|
||||||
|
service_id='a733d0e1-1ec9-4800-8df8-671e4affd017')
|
||||||
|
return sample
|
||||||
|
|
||||||
|
|
||||||
|
class FieldCollection(wtypes.Base):
|
||||||
|
"""Type describing a list of fields.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
fields = [Field]
|
||||||
|
"""List of fields."""
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def sample(cls):
|
||||||
|
sample = Field.sample()
|
||||||
|
return cls(fields=[sample])
|
|
@ -0,0 +1,58 @@
|
||||||
|
# -*- 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
|
||||||
|
#
|
||||||
|
from wsme import types as wtypes
|
||||||
|
|
||||||
|
from cloudkitty.api.v1 import types as ck_types
|
||||||
|
|
||||||
|
|
||||||
|
class Group(wtypes.Base):
|
||||||
|
"""Type describing a group.
|
||||||
|
|
||||||
|
A group is used to divide calculations. It can be used to create a group
|
||||||
|
for the instance rating (flavor) and one if we have premium images
|
||||||
|
(image_id). So you can take into account multiple parameters during the
|
||||||
|
rating.
|
||||||
|
"""
|
||||||
|
|
||||||
|
group_id = wtypes.wsattr(ck_types.UuidType(),
|
||||||
|
mandatory=False,
|
||||||
|
readonly=True)
|
||||||
|
"""UUID of the group."""
|
||||||
|
|
||||||
|
name = wtypes.wsattr(wtypes.text, mandatory=True)
|
||||||
|
"""Name of the group."""
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def sample(cls):
|
||||||
|
sample = cls(group_id='afe898cb-86d8-4557-ad67-f4f01891bbee',
|
||||||
|
name='instance_rating')
|
||||||
|
return sample
|
||||||
|
|
||||||
|
|
||||||
|
class GroupCollection(wtypes.Base):
|
||||||
|
"""Type describing a list of groups.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
groups = [Group]
|
||||||
|
"""List of groups."""
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def sample(cls):
|
||||||
|
sample = Group.sample()
|
||||||
|
return cls(groups=[sample])
|
|
@ -0,0 +1,82 @@
|
||||||
|
# -*- 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
|
||||||
|
|
||||||
|
MAP_TYPE = wtypes.Enum(wtypes.text, 'flat', 'rate')
|
||||||
|
|
||||||
|
|
||||||
|
class Mapping(wtypes.Base):
|
||||||
|
"""Type describing a Mapping.
|
||||||
|
|
||||||
|
A mapping is used to apply rating rules based on a value, if the parent is
|
||||||
|
a field then it's check the value of a metadata. If it's a service then it
|
||||||
|
directly apply the rate to the volume.
|
||||||
|
"""
|
||||||
|
|
||||||
|
mapping_id = wtypes.wsattr(ck_types.UuidType(),
|
||||||
|
mandatory=False,
|
||||||
|
readonly=True)
|
||||||
|
"""UUID of the mapping."""
|
||||||
|
|
||||||
|
value = wtypes.wsattr(wtypes.text, mandatory=False)
|
||||||
|
"""Key of the mapping."""
|
||||||
|
|
||||||
|
map_type = wtypes.wsattr(MAP_TYPE, default='flat', name='type')
|
||||||
|
"""Type of the mapping."""
|
||||||
|
|
||||||
|
cost = wtypes.wsattr(decimal.Decimal, mandatory=True)
|
||||||
|
"""Value of the mapping."""
|
||||||
|
|
||||||
|
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(mapping_id='39dbd39d-f663-4444-a795-fb19d81af136',
|
||||||
|
field_id='ac55b000-a05b-4832-b2ff-265a034886ab',
|
||||||
|
value='m1.micro',
|
||||||
|
map_type='flat',
|
||||||
|
cost=decimal.Decimal('4.2'))
|
||||||
|
return sample
|
||||||
|
|
||||||
|
|
||||||
|
class MappingCollection(wtypes.Base):
|
||||||
|
"""Type describing a list of mappings.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
mappings = [Mapping]
|
||||||
|
"""List of mappings."""
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def sample(cls):
|
||||||
|
sample = Mapping.sample()
|
||||||
|
return cls(mappings=[sample])
|
|
@ -0,0 +1,55 @@
|
||||||
|
# -*- 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
|
||||||
|
#
|
||||||
|
from wsme import types as wtypes
|
||||||
|
|
||||||
|
from cloudkitty.api.v1 import types as ck_types
|
||||||
|
|
||||||
|
|
||||||
|
class Service(wtypes.Base):
|
||||||
|
"""Type describing a service.
|
||||||
|
|
||||||
|
A service is directly mapped to the usage key, the collected service.
|
||||||
|
"""
|
||||||
|
|
||||||
|
service_id = wtypes.wsattr(ck_types.UuidType(),
|
||||||
|
mandatory=False,
|
||||||
|
readonly=True)
|
||||||
|
"""UUID of the service."""
|
||||||
|
|
||||||
|
name = wtypes.wsattr(wtypes.text, mandatory=True)
|
||||||
|
"""Name of the service."""
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def sample(cls):
|
||||||
|
sample = cls(service_id='a733d0e1-1ec9-4800-8df8-671e4affd017',
|
||||||
|
name='compute')
|
||||||
|
return sample
|
||||||
|
|
||||||
|
|
||||||
|
class ServiceCollection(wtypes.Base):
|
||||||
|
"""Type describing a list of services.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
services = [Service]
|
||||||
|
"""List of services."""
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def sample(cls):
|
||||||
|
sample = Service.sample()
|
||||||
|
return cls(services=[sample])
|
|
@ -35,59 +35,98 @@ def get_instance():
|
||||||
class NoSuchService(Exception):
|
class NoSuchService(Exception):
|
||||||
"""Raised when the service doesn't exist."""
|
"""Raised when the service doesn't exist."""
|
||||||
|
|
||||||
def __init__(self, service):
|
def __init__(self, name=None, uuid=None):
|
||||||
super(NoSuchService, self).__init__(
|
super(NoSuchService, self).__init__(
|
||||||
"No such service: %s" % service)
|
"No such service: %s (UUID: %s)" % (name, uuid))
|
||||||
self.service = service
|
self.name = name
|
||||||
|
self.uuid = uuid
|
||||||
|
|
||||||
|
|
||||||
class NoSuchField(Exception):
|
class NoSuchField(Exception):
|
||||||
"""Raised when the field doesn't exist for the service."""
|
"""Raised when the field doesn't exist for the service."""
|
||||||
|
|
||||||
def __init__(self, service, field):
|
def __init__(self, uuid):
|
||||||
super(NoSuchField, self).__init__(
|
super(NoSuchField, self).__init__(
|
||||||
"No such field for %s service: %s" % (service, field,))
|
"No such field: %s" % uuid)
|
||||||
self.service = service
|
self.uuid = uuid
|
||||||
self.field = field
|
|
||||||
|
|
||||||
|
class NoSuchGroup(Exception):
|
||||||
|
"""Raised when the group doesn't exist."""
|
||||||
|
|
||||||
|
def __init__(self, name=None, uuid=None):
|
||||||
|
super(NoSuchGroup, self).__init__(
|
||||||
|
"No such group: %s (UUID: %s)" % (name, uuid))
|
||||||
|
self.name = name
|
||||||
|
self.uuid = uuid
|
||||||
|
|
||||||
|
|
||||||
class NoSuchMapping(Exception):
|
class NoSuchMapping(Exception):
|
||||||
"""Raised when the mapping doesn't exist."""
|
"""Raised when the mapping doesn't exist."""
|
||||||
|
|
||||||
def __init__(self, service, field, key):
|
def __init__(self, uuid):
|
||||||
super(NoSuchMapping, self).__init__(
|
msg = ("No such mapping: %s" % uuid)
|
||||||
"No such key for %s service and %s field: %s"
|
super(NoSuchMapping, self).__init__(msg)
|
||||||
% (service, field, key,))
|
self.uuid = uuid
|
||||||
self.service = service
|
|
||||||
self.field = field
|
|
||||||
self.key = key
|
class NoSuchType(Exception):
|
||||||
|
"""Raised when a mapping type is not handled."""
|
||||||
|
|
||||||
|
def __init__(self, map_type):
|
||||||
|
msg = ("No mapping type: %s"
|
||||||
|
% (map_type))
|
||||||
|
super(NoSuchType, self).__init__(msg)
|
||||||
|
self.map_type = map_type
|
||||||
|
|
||||||
|
|
||||||
class ServiceAlreadyExists(Exception):
|
class ServiceAlreadyExists(Exception):
|
||||||
"""Raised when the service already exists."""
|
"""Raised when the service already exists."""
|
||||||
|
|
||||||
def __init__(self, service):
|
def __init__(self, name, uuid):
|
||||||
super(ServiceAlreadyExists, self).__init__(
|
super(ServiceAlreadyExists, self).__init__(
|
||||||
"Service %s already exists" % service)
|
"Service %s already exists (UUID: %s)" % (name, uuid))
|
||||||
self.service = service
|
self.name = name
|
||||||
|
self.uuid = uuid
|
||||||
|
|
||||||
|
|
||||||
class FieldAlreadyExists(Exception):
|
class FieldAlreadyExists(Exception):
|
||||||
"""Raised when the field already exists."""
|
"""Raised when the field already exists."""
|
||||||
|
|
||||||
def __init__(self, field):
|
def __init__(self, field, uuid):
|
||||||
super(FieldAlreadyExists, self).__init__(
|
super(FieldAlreadyExists, self).__init__(
|
||||||
"Field %s already exists" % field)
|
"Field %s already exists (UUID: %s)" % (field, uuid))
|
||||||
self.field = field
|
self.field = field
|
||||||
|
self.uuid = uuid
|
||||||
|
|
||||||
|
|
||||||
|
class GroupAlreadyExists(Exception):
|
||||||
|
"""Raised when the group already exists."""
|
||||||
|
|
||||||
|
def __init__(self, name, uuid):
|
||||||
|
super(GroupAlreadyExists, self).__init__(
|
||||||
|
"Group %s already exists (UUID: %s)" % (name, uuid))
|
||||||
|
self.name = name
|
||||||
|
self.uuid = uuid
|
||||||
|
|
||||||
|
|
||||||
class MappingAlreadyExists(Exception):
|
class MappingAlreadyExists(Exception):
|
||||||
"""Raised when the mapping already exists."""
|
"""Raised when the mapping already exists."""
|
||||||
|
|
||||||
def __init__(self, mapping):
|
def __init__(self, mapping, uuid):
|
||||||
super(MappingAlreadyExists, self).__init__(
|
super(MappingAlreadyExists, self).__init__(
|
||||||
"Mapping %s already exists" % mapping)
|
"Mapping %s already exists (UUID: %s)" % (mapping, uuid))
|
||||||
self.mapping = mapping
|
self.mapping = mapping
|
||||||
|
self.uuid = uuid
|
||||||
|
|
||||||
|
|
||||||
|
class MappingHasNoGroup(Exception):
|
||||||
|
"""Raised when the mapping is not attached to a group."""
|
||||||
|
|
||||||
|
def __init__(self, uuid):
|
||||||
|
super(MappingHasNoGroup, self).__init__(
|
||||||
|
"Mapping has no group (UUID: %s)" % uuid)
|
||||||
|
self.uuid = uuid
|
||||||
|
|
||||||
|
|
||||||
@six.add_metaclass(abc.ABCMeta)
|
@six.add_metaclass(abc.ABCMeta)
|
||||||
|
@ -101,119 +140,147 @@ class HashMap(object):
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@abc.abstractmethod
|
@abc.abstractmethod
|
||||||
def get_service(self, service):
|
def get_service(self, name=None, uuid=None):
|
||||||
"""Return a service object.
|
"""Return a service object.
|
||||||
|
|
||||||
:param service: The service to filter on.
|
:param name: Filter on a service name.
|
||||||
|
:param uuid: The uuid of the service to get.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@abc.abstractmethod
|
@abc.abstractmethod
|
||||||
def get_field(self, service, field):
|
def get_field(self, uuid=None, service_uuid=None, name=None):
|
||||||
"""Return a field object.
|
"""Return a field object.
|
||||||
|
|
||||||
:param service: The service to filter on.
|
:param uuid: UUID of the field to get.
|
||||||
:param field: The field to filter on.
|
:param service_uuid: UUID of the service to filter on. (Used with name)
|
||||||
|
:param name: Name of the field to filter on. (Used with service_uuid)
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@abc.abstractmethod
|
@abc.abstractmethod
|
||||||
def get_mapping(self, service, field, key):
|
def get_group(self, uuid):
|
||||||
"""Return a field object.
|
"""Return a group object.
|
||||||
|
|
||||||
:param service: The service to filter on.
|
:param uuid: UUID of the group to get.
|
||||||
:param field: The field to filter on.
|
"""
|
||||||
:param key: The field to filter on.
|
|
||||||
:param key: Value of the field to filter on.
|
@abc.abstractmethod
|
||||||
|
def get_mapping(self, uuid):
|
||||||
|
"""Return a mapping object.
|
||||||
|
|
||||||
|
:param uuid: UUID of the mapping to get.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@abc.abstractmethod
|
@abc.abstractmethod
|
||||||
def list_services(self):
|
def list_services(self):
|
||||||
"""Return a list of every services.
|
"""Return an UUID list of every service.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@abc.abstractmethod
|
@abc.abstractmethod
|
||||||
def list_fields(self, service):
|
def list_fields(self, service_uuid):
|
||||||
"""Return a list of every fields in a service.
|
"""Return an UUID list of every field in a service.
|
||||||
|
|
||||||
:param service: The service to filter on.
|
:param service_uuid: The service UUID to filter on.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@abc.abstractmethod
|
@abc.abstractmethod
|
||||||
def list_mappings(self, service, field):
|
def list_groups(self):
|
||||||
"""Return a list of every mapping.
|
"""Return an UUID list of every group.
|
||||||
|
|
||||||
:param service: The service to filter on.
|
|
||||||
:param field: The key to filter on.
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@abc.abstractmethod
|
@abc.abstractmethod
|
||||||
def create_service(self, service):
|
def list_mappings(self,
|
||||||
|
service_uuid=None,
|
||||||
|
field_uuid=None,
|
||||||
|
group_uuid=None,
|
||||||
|
no_group=False):
|
||||||
|
"""Return an UUID list of every mapping.
|
||||||
|
|
||||||
|
: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 mappings without a group.
|
||||||
|
|
||||||
|
:return list(str): List of mappings' UUID.
|
||||||
|
"""
|
||||||
|
|
||||||
|
@abc.abstractmethod
|
||||||
|
def create_service(self, name):
|
||||||
"""Create a new service.
|
"""Create a new service.
|
||||||
|
|
||||||
:param service:
|
:param name: Name of the service to create.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@abc.abstractmethod
|
@abc.abstractmethod
|
||||||
def create_field(self, service, field):
|
def create_field(self, service_uuid, name):
|
||||||
"""Create a new field.
|
"""Create a new field.
|
||||||
|
|
||||||
:param service:
|
:param service_uuid: UUID of the parent service.
|
||||||
:param field:
|
:param name: Name of the field.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@abc.abstractmethod
|
@abc.abstractmethod
|
||||||
def create_mapping(self, service, field, key, value, map_type='rate'):
|
def create_group(self, name):
|
||||||
|
"""Create a new group.
|
||||||
|
|
||||||
|
:param name: The name of the group.
|
||||||
|
"""
|
||||||
|
|
||||||
|
@abc.abstractmethod
|
||||||
|
def create_mapping(self,
|
||||||
|
cost,
|
||||||
|
map_type='rate',
|
||||||
|
value=None,
|
||||||
|
service_id=None,
|
||||||
|
field_id=None,
|
||||||
|
group_id=None):
|
||||||
"""Create a new service/field mapping.
|
"""Create a new service/field mapping.
|
||||||
|
|
||||||
:param service: Service the mapping is applying to.
|
:param cost: Rating value to apply to this mapping.
|
||||||
:param field: Field the mapping is applying to.
|
:param map_type: The type of rating rule.
|
||||||
:param key: Value of the field this mapping is applying to.
|
:param value: Value of the field this mapping is applying to.
|
||||||
:param value: Pricing value to apply to this mapping.
|
:param service_id: Service the mapping is applying to.
|
||||||
:param map_type: The type of pricing rule.
|
:param field_id: Field the mapping is applying to.
|
||||||
|
:param group_id: The group of calculations to apply.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@abc.abstractmethod
|
@abc.abstractmethod
|
||||||
def update_mapping(self, service, field, key, **kwargs):
|
def update_mapping(self, uuid, **kwargs):
|
||||||
"""Update a mapping.
|
"""Update a mapping.
|
||||||
|
|
||||||
:param service: Service the mapping is applying to.
|
:param uuid UUID of the mapping to modify.
|
||||||
:param field: Field the mapping is applying to.
|
:param cost: Rating value to apply to this mapping.
|
||||||
:param key: Value of the field this mapping is applying to.
|
:param map_type: The type of rating rule.
|
||||||
:param value: Pricing value to apply to this mapping.
|
:param value: Value of the field this mapping is applying to.
|
||||||
:param map_type: The type of pricing rule.
|
:param group_id: The group of calculations to apply.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@abc.abstractmethod
|
@abc.abstractmethod
|
||||||
def update_or_create_mapping(self, service, field, key, **kwargs):
|
def delete_service(self, name=None, uuid=None):
|
||||||
"""Update or create a mapping.
|
|
||||||
|
|
||||||
:param service: Service the mapping is applying to.
|
|
||||||
:param field: Field the mapping is applying to.
|
|
||||||
:param key: Value of the field this mapping is applying to.
|
|
||||||
:param value: Pricing value to apply to this mapping.
|
|
||||||
:param map_type: The type of pricing rule.
|
|
||||||
"""
|
|
||||||
|
|
||||||
@abc.abstractmethod
|
|
||||||
def delete_service(self, service):
|
|
||||||
"""Delete a service recursively.
|
"""Delete a service recursively.
|
||||||
|
|
||||||
:param service: Service to delete.
|
:param name: Name of the service to delete.
|
||||||
|
:param uuid: UUID of the service to delete.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@abc.abstractmethod
|
@abc.abstractmethod
|
||||||
def delete_field(self, service, field):
|
def delete_field(self, uuid):
|
||||||
"""Delete a field recursively.
|
"""Delete a field recursively.
|
||||||
|
|
||||||
:param service: Service the field is applying to.
|
:param uuid UUID of the field to delete.
|
||||||
:param field: field to delete.
|
"""
|
||||||
|
|
||||||
|
def delete_group(self, uuid, recurse=True):
|
||||||
|
"""Delete a group and all mappings recursively.
|
||||||
|
|
||||||
|
:param uuid: UUID of the group to delete.
|
||||||
|
:param recurse: Delete attached mappings recursively.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@abc.abstractmethod
|
@abc.abstractmethod
|
||||||
def delete_mapping(self, service, field, key):
|
def delete_mapping(self, uuid):
|
||||||
"""Delete a mapping recursively.
|
"""Delete a mapping
|
||||||
|
|
||||||
:param service: Service the field is applying to.
|
:param uuid: UUID of the mapping to delete.
|
||||||
:param field: Field the mapping is applying to.
|
|
||||||
:param key: key to delete.
|
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -0,0 +1,81 @@
|
||||||
|
"""Initial migration
|
||||||
|
|
||||||
|
Revision ID: 3dd7e13527f3
|
||||||
|
Revises: None
|
||||||
|
Create Date: 2015-03-10 13:06:41.067563
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = '3dd7e13527f3'
|
||||||
|
down_revision = None
|
||||||
|
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade():
|
||||||
|
op.create_table('hashmap_services',
|
||||||
|
sa.Column('id', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('service_id', sa.String(length=36), nullable=False),
|
||||||
|
sa.Column('name', sa.String(length=255), nullable=False),
|
||||||
|
sa.PrimaryKeyConstraint('id'),
|
||||||
|
sa.UniqueConstraint('name'),
|
||||||
|
sa.UniqueConstraint('service_id'),
|
||||||
|
mysql_charset='utf8',
|
||||||
|
mysql_engine='InnoDB'
|
||||||
|
)
|
||||||
|
op.create_table('hashmap_groups',
|
||||||
|
sa.Column('id', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('group_id', sa.String(length=36), nullable=False),
|
||||||
|
sa.Column('name', sa.String(length=255), nullable=False),
|
||||||
|
sa.PrimaryKeyConstraint('id'),
|
||||||
|
sa.UniqueConstraint('group_id'),
|
||||||
|
sa.UniqueConstraint('name'),
|
||||||
|
mysql_charset='utf8',
|
||||||
|
mysql_engine='InnoDB'
|
||||||
|
)
|
||||||
|
op.create_table('hashmap_fields',
|
||||||
|
sa.Column('id', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('field_id', sa.String(length=36), nullable=False),
|
||||||
|
sa.Column('name', sa.String(length=255), nullable=False),
|
||||||
|
sa.Column('service_id', sa.Integer(), nullable=False),
|
||||||
|
sa.ForeignKeyConstraint(['service_id'], ['hashmap_services.id'],
|
||||||
|
ondelete='CASCADE'),
|
||||||
|
sa.PrimaryKeyConstraint('id'),
|
||||||
|
sa.UniqueConstraint('field_id'),
|
||||||
|
sa.UniqueConstraint('field_id', 'name', name='uniq_field'),
|
||||||
|
sa.UniqueConstraint('service_id', 'name', name='uniq_map_service_field'),
|
||||||
|
mysql_charset='utf8',
|
||||||
|
mysql_engine='InnoDB'
|
||||||
|
)
|
||||||
|
op.create_table('hashmap_maps',
|
||||||
|
sa.Column('id', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('mapping_id', sa.String(length=36), nullable=False),
|
||||||
|
sa.Column('value', sa.String(length=255), nullable=True),
|
||||||
|
sa.Column('cost', sa.Numeric(20, 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('mapping_id'),
|
||||||
|
sa.UniqueConstraint('value', 'field_id', name='uniq_field_mapping'),
|
||||||
|
sa.UniqueConstraint('value', 'service_id', name='uniq_service_mapping'),
|
||||||
|
mysql_charset='utf8',
|
||||||
|
mysql_engine='InnoDB'
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade():
|
||||||
|
op.drop_table('hashmap_maps')
|
||||||
|
op.drop_table('hashmap_fields')
|
||||||
|
op.drop_table('hashmap_groups')
|
||||||
|
op.drop_table('hashmap_services')
|
|
@ -1,60 +0,0 @@
|
||||||
"""initial migration
|
|
||||||
|
|
||||||
Revision ID: 48676342515a
|
|
||||||
Revises: None
|
|
||||||
Create Date: 2014-08-05 17:13:10.323228
|
|
||||||
|
|
||||||
"""
|
|
||||||
|
|
||||||
# revision identifiers, used by Alembic.
|
|
||||||
revision = '48676342515a'
|
|
||||||
down_revision = None
|
|
||||||
|
|
||||||
from alembic import op
|
|
||||||
import sqlalchemy as sa
|
|
||||||
|
|
||||||
|
|
||||||
def upgrade():
|
|
||||||
### commands auto generated by Alembic - please adjust! ###
|
|
||||||
op.create_table('hashmap_services',
|
|
||||||
sa.Column('id', sa.Integer(), nullable=False),
|
|
||||||
sa.Column('name', sa.String(length=255), nullable=False),
|
|
||||||
sa.PrimaryKeyConstraint('id'),
|
|
||||||
sa.UniqueConstraint('name'),
|
|
||||||
mysql_charset='utf8',
|
|
||||||
mysql_engine='InnoDB'
|
|
||||||
)
|
|
||||||
op.create_table('hashmap_fields',
|
|
||||||
sa.Column('id', sa.Integer(), nullable=False),
|
|
||||||
sa.Column('name', sa.String(length=255), nullable=False),
|
|
||||||
sa.Column('service_id', sa.Integer(), nullable=False),
|
|
||||||
sa.ForeignKeyConstraint(['service_id'], ['hashmap_services.id'], ondelete='CASCADE'),
|
|
||||||
sa.PrimaryKeyConstraint('id'),
|
|
||||||
sa.UniqueConstraint('service_id', 'name', name='uniq_map_service_field'),
|
|
||||||
mysql_charset='utf8',
|
|
||||||
mysql_engine='InnoDB'
|
|
||||||
)
|
|
||||||
op.create_table('hashmap_maps',
|
|
||||||
sa.Column('id', sa.Integer(), nullable=False),
|
|
||||||
sa.Column('key', sa.String(length=255), nullable=False),
|
|
||||||
sa.Column('value', sa.Float(), nullable=False),
|
|
||||||
sa.Column('map_type', sa.Enum('flat', 'rate', name='enum_map_type'), nullable=False),
|
|
||||||
sa.Column('field_id', sa.Integer(), nullable=False),
|
|
||||||
sa.ForeignKeyConstraint(['field_id'], ['hashmap_fields.id'], ondelete='CASCADE'),
|
|
||||||
sa.PrimaryKeyConstraint('id'),
|
|
||||||
sa.UniqueConstraint('key', 'field_id', name='uniq_mapping'),
|
|
||||||
mysql_charset='utf8',
|
|
||||||
mysql_engine='InnoDB'
|
|
||||||
)
|
|
||||||
### end Alembic commands ###
|
|
||||||
|
|
||||||
|
|
||||||
def downgrade():
|
|
||||||
### commands auto generated by Alembic - please adjust! ###
|
|
||||||
op.create_table('hashmap_alembic',
|
|
||||||
sa.Column('version_num', sa.VARCHAR(length=32), nullable=False)
|
|
||||||
)
|
|
||||||
op.drop_table('hashmap_maps')
|
|
||||||
op.drop_table('hashmap_fields')
|
|
||||||
op.drop_table('hashmap_services')
|
|
||||||
### end Alembic commands ###
|
|
|
@ -17,6 +17,7 @@
|
||||||
#
|
#
|
||||||
from oslo.db import exception
|
from oslo.db import exception
|
||||||
from oslo.db.sqlalchemy import utils
|
from oslo.db.sqlalchemy import utils
|
||||||
|
from oslo.utils import uuidutils
|
||||||
import six
|
import six
|
||||||
import sqlalchemy
|
import sqlalchemy
|
||||||
|
|
||||||
|
@ -38,189 +39,309 @@ class HashMap(api.HashMap):
|
||||||
def get_migration(self):
|
def get_migration(self):
|
||||||
return migration
|
return migration
|
||||||
|
|
||||||
def get_service(self, service):
|
def get_service(self, name=None, uuid=None):
|
||||||
session = db.get_session()
|
session = db.get_session()
|
||||||
try:
|
try:
|
||||||
q = session.query(models.HashMapService)
|
q = session.query(models.HashMapService)
|
||||||
res = q.filter_by(
|
if name:
|
||||||
name=service,
|
q = q.filter(
|
||||||
).one()
|
models.HashMapService.name == name)
|
||||||
|
elif uuid:
|
||||||
|
q = q.filter(
|
||||||
|
models.HashMapService.service_id == uuid)
|
||||||
|
else:
|
||||||
|
raise ValueError('You must specify either name or uuid.')
|
||||||
|
res = q.one()
|
||||||
return res
|
return res
|
||||||
except sqlalchemy.orm.exc.NoResultFound:
|
except sqlalchemy.orm.exc.NoResultFound:
|
||||||
raise api.NoSuchService(service)
|
raise api.NoSuchService(name=name, uuid=uuid)
|
||||||
|
|
||||||
def get_field(self, service, field):
|
def get_field(self, uuid=None, service_uuid=None, name=None):
|
||||||
session = db.get_session()
|
session = db.get_session()
|
||||||
try:
|
try:
|
||||||
service_db = self.get_service(service)
|
|
||||||
q = session.query(models.HashMapField)
|
q = session.query(models.HashMapField)
|
||||||
res = q.filter_by(
|
if uuid:
|
||||||
service_id=service_db.id,
|
q = q.filter(
|
||||||
name=field
|
models.HashMapField.field_id == uuid)
|
||||||
).one()
|
elif service_uuid and name:
|
||||||
|
q = q.join(
|
||||||
|
models.HashMapField.service)
|
||||||
|
q = q.filter(
|
||||||
|
models.HashMapService.service_id == service_uuid,
|
||||||
|
models.HashMapField.name == name)
|
||||||
|
else:
|
||||||
|
raise ValueError('You must specify either an uuid'
|
||||||
|
' or a service_uuid and a name.')
|
||||||
|
res = q.one()
|
||||||
return res
|
return res
|
||||||
except sqlalchemy.orm.exc.NoResultFound:
|
except sqlalchemy.orm.exc.NoResultFound:
|
||||||
raise api.NoSuchField(service, field)
|
raise api.NoSuchField(uuid)
|
||||||
|
|
||||||
def get_mapping(self, service, field, key):
|
def get_group(self, uuid):
|
||||||
session = db.get_session()
|
session = db.get_session()
|
||||||
try:
|
try:
|
||||||
field_db = self.get_field(service, field)
|
q = session.query(models.HashMapGroup)
|
||||||
q = session.query(models.HashMapMapping)
|
q = q.filter(
|
||||||
res = q.filter_by(
|
models.HashMapGroup.group_id == uuid)
|
||||||
key=key,
|
res = q.one()
|
||||||
field_id=field_db.id
|
|
||||||
).one()
|
|
||||||
return res
|
return res
|
||||||
except sqlalchemy.orm.exc.NoResultFound:
|
except sqlalchemy.orm.exc.NoResultFound:
|
||||||
raise api.NoSuchMapping(service, field, key)
|
raise api.NoSuchGroup(uuid=uuid)
|
||||||
|
|
||||||
|
def get_mapping(self, uuid):
|
||||||
|
session = db.get_session()
|
||||||
|
try:
|
||||||
|
q = session.query(models.HashMapMapping)
|
||||||
|
q = q.filter(
|
||||||
|
models.HashMapMapping.mapping_id == uuid)
|
||||||
|
res = q.one()
|
||||||
|
return res
|
||||||
|
except sqlalchemy.orm.exc.NoResultFound:
|
||||||
|
raise api.NoSuchMapping(uuid)
|
||||||
|
|
||||||
|
def get_group_from_mapping(self, uuid):
|
||||||
|
session = db.get_session()
|
||||||
|
try:
|
||||||
|
q = session.query(models.HashMapGroup)
|
||||||
|
q = q.join(
|
||||||
|
models.HashMapGroup.mappings)
|
||||||
|
q = q.filter(
|
||||||
|
models.HashMapMapping.mapping_id == uuid)
|
||||||
|
res = q.one()
|
||||||
|
return res
|
||||||
|
except sqlalchemy.orm.exc.NoResultFound:
|
||||||
|
raise api.MappingHasNoGroup(uuid=uuid)
|
||||||
|
|
||||||
def list_services(self):
|
def list_services(self):
|
||||||
session = db.get_session()
|
session = db.get_session()
|
||||||
q = session.query(models.HashMapService)
|
q = session.query(models.HashMapService)
|
||||||
res = q.values(
|
res = q.values(
|
||||||
models.HashMapService.name
|
models.HashMapService.service_id)
|
||||||
)
|
return [uuid[0] for uuid in res]
|
||||||
return res
|
|
||||||
|
|
||||||
def list_fields(self, service):
|
def list_fields(self, service_uuid):
|
||||||
session = db.get_session()
|
session = db.get_session()
|
||||||
service_db = self.get_service(service)
|
|
||||||
q = session.query(models.HashMapField)
|
q = session.query(models.HashMapField)
|
||||||
res = q.filter_by(
|
q = q.join(
|
||||||
service_id=service_db.id
|
models.HashMapField.service)
|
||||||
).values(
|
q = q.filter(
|
||||||
models.HashMapField.name
|
models.HashMapService.service_id == service_uuid)
|
||||||
)
|
res = q.values(models.HashMapField.field_id)
|
||||||
return res
|
return [uuid[0] for uuid in res]
|
||||||
|
|
||||||
def list_mappings(self, service, field):
|
def list_groups(self):
|
||||||
session = db.get_session()
|
session = db.get_session()
|
||||||
field_db = self.get_field(service, field)
|
q = session.query(models.HashMapGroup)
|
||||||
q = session.query(models.HashMapMapping)
|
res = q.values(
|
||||||
res = q.filter_by(
|
models.HashMapGroup.group_id)
|
||||||
field_id=field_db.id
|
return [uuid[0] for uuid in res]
|
||||||
).values(
|
|
||||||
models.HashMapMapping.key
|
|
||||||
)
|
|
||||||
return res
|
|
||||||
|
|
||||||
def create_service(self, service):
|
def list_mappings(self,
|
||||||
|
service_uuid=None,
|
||||||
|
field_uuid=None,
|
||||||
|
group_uuid=None,
|
||||||
|
no_group=False):
|
||||||
|
|
||||||
|
session = db.get_session()
|
||||||
|
q = session.query(models.HashMapMapping)
|
||||||
|
if service_uuid:
|
||||||
|
q = q.join(
|
||||||
|
models.HashMapMapping.service)
|
||||||
|
q = q.filter(
|
||||||
|
models.HashMapService.service_id == service_uuid)
|
||||||
|
elif field_uuid:
|
||||||
|
q = q.join(
|
||||||
|
models.HashMapMapping.field)
|
||||||
|
q = q.filter(models.HashMapField.field_id == field_uuid)
|
||||||
|
if group_uuid:
|
||||||
|
q = q.join(
|
||||||
|
models.HashMapMapping.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.HashMapMapping.group_id == None) # noqa
|
||||||
|
res = q.values(
|
||||||
|
models.HashMapMapping.mapping_id
|
||||||
|
)
|
||||||
|
return [uuid[0] for uuid in res]
|
||||||
|
|
||||||
|
def create_service(self, name):
|
||||||
session = db.get_session()
|
session = db.get_session()
|
||||||
try:
|
try:
|
||||||
with session.begin():
|
with session.begin():
|
||||||
service_db = models.HashMapService(name=service)
|
service_db = models.HashMapService(name=name)
|
||||||
|
service_db.service_id = uuidutils.generate_uuid()
|
||||||
session.add(service_db)
|
session.add(service_db)
|
||||||
session.flush()
|
|
||||||
# TODO(sheeprine): return object
|
|
||||||
return service_db
|
return service_db
|
||||||
except exception.DBDuplicateEntry:
|
except exception.DBDuplicateEntry:
|
||||||
raise api.ServiceAlreadyExists(service)
|
service_db = self.get_service(name=name)
|
||||||
|
raise api.ServiceAlreadyExists(
|
||||||
|
service_db.name,
|
||||||
|
service_db.service_id)
|
||||||
|
|
||||||
def create_field(self, service, field):
|
def create_field(self, service_uuid, name):
|
||||||
try:
|
service_db = self.get_service(uuid=service_uuid)
|
||||||
service_db = self.get_service(service)
|
|
||||||
except api.NoSuchService:
|
|
||||||
service_db = self.create_service(service)
|
|
||||||
session = db.get_session()
|
session = db.get_session()
|
||||||
try:
|
try:
|
||||||
with session.begin():
|
with session.begin():
|
||||||
field_db = models.HashMapField(
|
field_db = models.HashMapField(
|
||||||
service_id=service_db.id,
|
service_id=service_db.id,
|
||||||
name=field)
|
name=name,
|
||||||
|
field_id=uuidutils.generate_uuid())
|
||||||
session.add(field_db)
|
session.add(field_db)
|
||||||
session.flush()
|
|
||||||
# TODO(sheeprine): return object
|
|
||||||
return field_db
|
return field_db
|
||||||
except exception.DBDuplicateEntry:
|
except exception.DBDuplicateEntry:
|
||||||
raise api.FieldAlreadyExists(field)
|
field_db = self.get_field(service_uuid=service_uuid,
|
||||||
|
name=name)
|
||||||
|
raise api.FieldAlreadyExists(field_db.name, field_db.field_id)
|
||||||
|
|
||||||
def create_mapping(self, service, field, key, value, map_type='rate'):
|
def create_group(self, name):
|
||||||
|
session = db.get_session()
|
||||||
try:
|
try:
|
||||||
field_db = self.get_field(service, field)
|
with session.begin():
|
||||||
except (api.NoSuchField, api.NoSuchService):
|
group_db = models.HashMapGroup(
|
||||||
field_db = self.create_field(service, field)
|
name=name,
|
||||||
|
group_id=uuidutils.generate_uuid())
|
||||||
|
session.add(group_db)
|
||||||
|
return group_db
|
||||||
|
except exception.DBDuplicateEntry:
|
||||||
|
raise api.GroupAlreadyExists(name, group_db.group_id)
|
||||||
|
|
||||||
|
def create_mapping(self,
|
||||||
|
cost,
|
||||||
|
map_type='rate',
|
||||||
|
value=None,
|
||||||
|
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
|
||||||
|
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')
|
||||||
|
if group_id:
|
||||||
|
group_db = self.get_group(uuid=group_id)
|
||||||
session = db.get_session()
|
session = db.get_session()
|
||||||
try:
|
try:
|
||||||
with session.begin():
|
with session.begin():
|
||||||
field_map = models.HashMapMapping(
|
field_map = models.HashMapMapping(
|
||||||
field_id=field_db.id,
|
mapping_id=uuidutils.generate_uuid(),
|
||||||
key=key,
|
|
||||||
value=value,
|
value=value,
|
||||||
|
cost=cost,
|
||||||
|
field_id=field_fk,
|
||||||
|
service_id=service_fk,
|
||||||
map_type=map_type)
|
map_type=map_type)
|
||||||
|
if group_id:
|
||||||
|
field_map.group_id = group_db.id
|
||||||
session.add(field_map)
|
session.add(field_map)
|
||||||
# TODO(sheeprine): return object
|
|
||||||
return field_map
|
return field_map
|
||||||
except exception.DBDuplicateEntry:
|
except exception.DBDuplicateEntry:
|
||||||
raise api.MappingAlreadyExists(key)
|
raise api.MappingAlreadyExists(value, field_map.field_id)
|
||||||
|
except exception.DBError:
|
||||||
|
raise api.NoSuchType(map_type)
|
||||||
|
|
||||||
def update_mapping(self, service, field, key, **kwargs):
|
def update_mapping(self, uuid, **kwargs):
|
||||||
field_db = self.get_field(service, field)
|
|
||||||
session = db.get_session()
|
session = db.get_session()
|
||||||
try:
|
try:
|
||||||
with session.begin():
|
with session.begin():
|
||||||
q = session.query(models.HashMapMapping)
|
q = session.query(models.HashMapMapping)
|
||||||
field_map = q.filter_by(
|
q = q.filter(
|
||||||
key=key,
|
models.HashMapMapping.mapping_id == uuid
|
||||||
field_id=field_db.id
|
)
|
||||||
).with_lockmode('update').one()
|
mapping_db = q.with_lockmode('update').one()
|
||||||
if kwargs:
|
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)
|
||||||
|
mapping_db.group_id = group_db.id
|
||||||
|
# Service and Field shouldn't be updated
|
||||||
|
excluded_cols = ['mapping_id', 'service_id', 'field_id']
|
||||||
|
for col in excluded_cols:
|
||||||
|
if col in kwargs:
|
||||||
|
kwargs.pop(col)
|
||||||
for attribute, value in six.iteritems(kwargs):
|
for attribute, value in six.iteritems(kwargs):
|
||||||
if hasattr(field_map, attribute):
|
if hasattr(mapping_db, attribute):
|
||||||
setattr(field_map, attribute, value)
|
setattr(mapping_db, attribute, value)
|
||||||
else:
|
else:
|
||||||
raise ValueError('No such attribute: {}'.format(
|
raise ValueError('No such attribute: {}'.format(
|
||||||
attribute))
|
attribute))
|
||||||
else:
|
else:
|
||||||
raise ValueError('No attribute to update.')
|
raise ValueError('No attribute to update.')
|
||||||
return field_map
|
return mapping_db
|
||||||
except sqlalchemy.orm.exc.NoResultFound:
|
except sqlalchemy.orm.exc.NoResultFound:
|
||||||
raise api.NoSuchMapping(service, field, key)
|
raise api.NoSuchMapping(uuid)
|
||||||
|
|
||||||
def update_or_create_mapping(self, service, field, key, **kwargs):
|
def delete_service(self, name=None, uuid=None):
|
||||||
try:
|
|
||||||
return self.create_mapping(
|
|
||||||
service,
|
|
||||||
field,
|
|
||||||
key,
|
|
||||||
**kwargs
|
|
||||||
)
|
|
||||||
except api.MappingAlreadyExists:
|
|
||||||
return self.update_mapping(service, field, key, **kwargs)
|
|
||||||
|
|
||||||
def delete_service(self, service):
|
|
||||||
session = db.get_session()
|
session = db.get_session()
|
||||||
r = utils.model_query(
|
q = utils.model_query(
|
||||||
models.HashMapService,
|
models.HashMapService,
|
||||||
session
|
session
|
||||||
).filter_by(
|
)
|
||||||
name=service,
|
if name:
|
||||||
).delete()
|
q = q.filter_by(name=name)
|
||||||
|
elif uuid:
|
||||||
|
q = q.filter_by(service_id=uuid)
|
||||||
|
else:
|
||||||
|
raise ValueError('You must specify either name or uuid.')
|
||||||
|
r = q.delete()
|
||||||
if not r:
|
if not r:
|
||||||
raise api.NoSuchService(service)
|
raise api.NoSuchService(name, uuid)
|
||||||
|
|
||||||
def delete_field(self, service, field):
|
def delete_field(self, uuid):
|
||||||
session = db.get_session()
|
session = db.get_session()
|
||||||
service_db = self.get_service(service)
|
q = utils.model_query(
|
||||||
r = utils.model_query(
|
|
||||||
models.HashMapField,
|
models.HashMapField,
|
||||||
session
|
session
|
||||||
).filter_by(
|
)
|
||||||
service_id=service_db.id,
|
q = q.filter_by(
|
||||||
name=field,
|
field_id=uuid
|
||||||
).delete()
|
)
|
||||||
|
r = q.delete()
|
||||||
if not r:
|
if not r:
|
||||||
raise api.NoSuchField(service, field)
|
raise api.NoSuchField(uuid)
|
||||||
|
|
||||||
def delete_mapping(self, service, field, key):
|
def delete_group(self, uuid, recurse=True):
|
||||||
session = db.get_session()
|
session = db.get_session()
|
||||||
field = self.get_field(service, field)
|
q = utils.model_query(
|
||||||
r = utils.model_query(
|
models.HashMapGroup,
|
||||||
models.HashMapMapping,
|
|
||||||
session
|
session
|
||||||
).filter_by(
|
).filter_by(
|
||||||
field_id=field.id,
|
group_id=uuid,
|
||||||
key=key,
|
)
|
||||||
).delete()
|
with session.begin():
|
||||||
|
try:
|
||||||
|
r = q.with_lockmode('update').one()
|
||||||
|
except sqlalchemy.orm.exc.NoResultFound:
|
||||||
|
raise api.NoSuchGroup(uuid=uuid)
|
||||||
|
if recurse:
|
||||||
|
for mapping in r.mappings:
|
||||||
|
session.delete(mapping)
|
||||||
|
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
|
||||||
|
)
|
||||||
|
r = q.delete()
|
||||||
if not r:
|
if not r:
|
||||||
raise api.NoSuchMapping(service, field, key)
|
raise api.NoSuchMapping(uuid)
|
||||||
|
|
|
@ -21,35 +21,75 @@ from sqlalchemy.ext import declarative
|
||||||
from sqlalchemy import orm
|
from sqlalchemy import orm
|
||||||
from sqlalchemy import schema
|
from sqlalchemy import schema
|
||||||
|
|
||||||
|
|
||||||
Base = declarative.declarative_base()
|
Base = declarative.declarative_base()
|
||||||
|
|
||||||
|
|
||||||
class HashMapBase(models.ModelBase):
|
class HashMapBase(models.ModelBase):
|
||||||
__table_args__ = {'mysql_charset': "utf8",
|
__table_args__ = {'mysql_charset': "utf8",
|
||||||
'mysql_engine': "InnoDB"}
|
'mysql_engine': "InnoDB"}
|
||||||
|
fk_to_resolve = {}
|
||||||
|
|
||||||
|
def save(self, session=None):
|
||||||
|
from cloudkitty import db
|
||||||
|
|
||||||
|
if session is None:
|
||||||
|
session = db.get_session()
|
||||||
|
|
||||||
|
super(HashMapBase, self).save(session=session)
|
||||||
|
|
||||||
|
def as_dict(self):
|
||||||
|
d = {}
|
||||||
|
for c in self.__table__.columns:
|
||||||
|
if c.name == 'id':
|
||||||
|
continue
|
||||||
|
d[c.name] = self[c.name]
|
||||||
|
return d
|
||||||
|
|
||||||
|
def _recursive_resolve(self, path):
|
||||||
|
obj = self
|
||||||
|
for attr in path.split('.'):
|
||||||
|
if hasattr(obj, attr):
|
||||||
|
obj = getattr(obj, attr)
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
return obj
|
||||||
|
|
||||||
|
def export_model(self):
|
||||||
|
res = self.as_dict()
|
||||||
|
for fk, mapping in self.fk_to_resolve.items():
|
||||||
|
res[fk] = self._recursive_resolve(mapping)
|
||||||
|
return res
|
||||||
|
|
||||||
|
|
||||||
class HashMapService(Base, HashMapBase):
|
class HashMapService(Base, HashMapBase):
|
||||||
"""An hashmap service.
|
"""An hashmap service.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
__tablename__ = 'hashmap_services'
|
__tablename__ = 'hashmap_services'
|
||||||
|
|
||||||
id = sqlalchemy.Column(sqlalchemy.Integer,
|
id = sqlalchemy.Column(sqlalchemy.Integer,
|
||||||
primary_key=True)
|
primary_key=True)
|
||||||
|
service_id = sqlalchemy.Column(sqlalchemy.String(36),
|
||||||
|
nullable=False,
|
||||||
|
unique=True)
|
||||||
name = sqlalchemy.Column(
|
name = sqlalchemy.Column(
|
||||||
sqlalchemy.String(255),
|
sqlalchemy.String(255),
|
||||||
nullable=False,
|
nullable=False,
|
||||||
unique=True
|
unique=True
|
||||||
)
|
)
|
||||||
fields = orm.relationship('HashMapField')
|
fields = orm.relationship('HashMapField',
|
||||||
|
backref=orm.backref(
|
||||||
|
'service',
|
||||||
|
lazy='immediate'))
|
||||||
|
mappings = orm.relationship('HashMapMapping',
|
||||||
|
backref=orm.backref(
|
||||||
|
'service',
|
||||||
|
lazy='immediate'))
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return ('<HashMapService[{id}]: '
|
return ('<HashMapService[{uuid}]: '
|
||||||
'service={service}>').format(
|
'service={service}>').format(
|
||||||
id=self.id,
|
uuid=self.service_id,
|
||||||
service=self.name)
|
service=self.name)
|
||||||
|
|
||||||
|
|
||||||
|
@ -57,18 +97,23 @@ class HashMapField(Base, HashMapBase):
|
||||||
"""An hashmap field.
|
"""An hashmap field.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
__tablename__ = 'hashmap_fields'
|
__tablename__ = 'hashmap_fields'
|
||||||
|
fk_to_resolve = {'service_id': 'service.service_id'}
|
||||||
|
|
||||||
@declarative.declared_attr
|
@declarative.declared_attr
|
||||||
def __table_args__(cls):
|
def __table_args__(cls):
|
||||||
args = (schema.UniqueConstraint('service_id', 'name',
|
args = (schema.UniqueConstraint('field_id', 'name',
|
||||||
|
name='uniq_field'),
|
||||||
|
schema.UniqueConstraint('service_id', 'name',
|
||||||
name='uniq_map_service_field'),
|
name='uniq_map_service_field'),
|
||||||
HashMapBase.__table_args__,)
|
HashMapBase.__table_args__,)
|
||||||
return args
|
return args
|
||||||
|
|
||||||
id = sqlalchemy.Column(sqlalchemy.Integer,
|
id = sqlalchemy.Column(sqlalchemy.Integer,
|
||||||
primary_key=True)
|
primary_key=True)
|
||||||
|
field_id = sqlalchemy.Column(sqlalchemy.String(36),
|
||||||
|
nullable=False,
|
||||||
|
unique=True)
|
||||||
name = sqlalchemy.Column(sqlalchemy.String(255),
|
name = sqlalchemy.Column(sqlalchemy.String(255),
|
||||||
nullable=False)
|
nullable=False)
|
||||||
service_id = sqlalchemy.Column(
|
service_id = sqlalchemy.Column(
|
||||||
|
@ -77,48 +122,92 @@ class HashMapField(Base, HashMapBase):
|
||||||
ondelete='CASCADE'),
|
ondelete='CASCADE'),
|
||||||
nullable=False
|
nullable=False
|
||||||
)
|
)
|
||||||
field_maps = orm.relationship('HashMapMapping')
|
mappings = orm.relationship('HashMapMapping',
|
||||||
|
backref=orm.backref(
|
||||||
|
'field',
|
||||||
|
lazy='immediate'))
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return ('<HashMapField[{id}]: '
|
return ('<HashMapField[{uuid}]: '
|
||||||
'field={field}>').format(
|
'field={field}>').format(
|
||||||
id=self.id,
|
uuid=self.field_id,
|
||||||
field=self.field)
|
field=self.name)
|
||||||
|
|
||||||
|
|
||||||
|
class HashMapGroup(Base, HashMapBase):
|
||||||
|
"""A grouping of hashmap calculations.
|
||||||
|
|
||||||
|
"""
|
||||||
|
__tablename__ = 'hashmap_groups'
|
||||||
|
|
||||||
|
id = sqlalchemy.Column(sqlalchemy.Integer,
|
||||||
|
primary_key=True)
|
||||||
|
group_id = sqlalchemy.Column(sqlalchemy.String(36),
|
||||||
|
nullable=False,
|
||||||
|
unique=True)
|
||||||
|
name = sqlalchemy.Column(sqlalchemy.String(255),
|
||||||
|
nullable=False,
|
||||||
|
unique=True)
|
||||||
|
mappings = orm.relationship('HashMapMapping',
|
||||||
|
backref=orm.backref(
|
||||||
|
'group',
|
||||||
|
lazy='immediate'))
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return ('<HashMapGroup[{uuid}]: '
|
||||||
|
'name={name}>').format(
|
||||||
|
uuid=self.group_id,
|
||||||
|
name=self.name)
|
||||||
|
|
||||||
|
|
||||||
class HashMapMapping(Base, HashMapBase):
|
class HashMapMapping(Base, HashMapBase):
|
||||||
"""A mapping between a field a value and a type.
|
"""A mapping between a field a value and a type.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
__tablename__ = 'hashmap_maps'
|
__tablename__ = 'hashmap_maps'
|
||||||
|
fk_to_resolve = {'service_id': 'service.service_id',
|
||||||
|
'field_id': 'field.field_id',
|
||||||
|
'group_id': 'group.group_id'}
|
||||||
|
|
||||||
@declarative.declared_attr
|
@declarative.declared_attr
|
||||||
def __table_args__(cls):
|
def __table_args__(cls):
|
||||||
args = (schema.UniqueConstraint('key', 'field_id',
|
args = (schema.UniqueConstraint('value', 'field_id',
|
||||||
name='uniq_mapping'),
|
name='uniq_field_mapping'),
|
||||||
|
schema.UniqueConstraint('value', 'service_id',
|
||||||
|
name='uniq_service_mapping'),
|
||||||
HashMapBase.__table_args__,)
|
HashMapBase.__table_args__,)
|
||||||
return args
|
return args
|
||||||
|
|
||||||
id = sqlalchemy.Column(sqlalchemy.Integer,
|
id = sqlalchemy.Column(sqlalchemy.Integer,
|
||||||
primary_key=True)
|
primary_key=True)
|
||||||
key = sqlalchemy.Column(sqlalchemy.String(255),
|
mapping_id = sqlalchemy.Column(sqlalchemy.String(36),
|
||||||
nullable=False)
|
nullable=False,
|
||||||
value = sqlalchemy.Column(sqlalchemy.Float,
|
unique=True)
|
||||||
|
value = sqlalchemy.Column(sqlalchemy.String(255),
|
||||||
|
nullable=True)
|
||||||
|
cost = sqlalchemy.Column(sqlalchemy.Numeric(20, 8),
|
||||||
nullable=False)
|
nullable=False)
|
||||||
map_type = sqlalchemy.Column(sqlalchemy.Enum('flat',
|
map_type = sqlalchemy.Column(sqlalchemy.Enum('flat',
|
||||||
'rate',
|
'rate',
|
||||||
name='enum_map_type'),
|
name='enum_map_type'),
|
||||||
nullable=False)
|
nullable=False)
|
||||||
|
service_id = sqlalchemy.Column(sqlalchemy.Integer,
|
||||||
|
sqlalchemy.ForeignKey('hashmap_services.id',
|
||||||
|
ondelete='CASCADE'),
|
||||||
|
nullable=True)
|
||||||
field_id = sqlalchemy.Column(sqlalchemy.Integer,
|
field_id = sqlalchemy.Column(sqlalchemy.Integer,
|
||||||
sqlalchemy.ForeignKey('hashmap_fields.id',
|
sqlalchemy.ForeignKey('hashmap_fields.id',
|
||||||
ondelete='CASCADE'),
|
ondelete='CASCADE'),
|
||||||
nullable=False)
|
nullable=True)
|
||||||
|
group_id = sqlalchemy.Column(sqlalchemy.Integer,
|
||||||
|
sqlalchemy.ForeignKey('hashmap_groups.id',
|
||||||
|
ondelete='SET NULL'),
|
||||||
|
nullable=True)
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return ('<HashMapMapping[{id}]: '
|
return ('<HashMapMapping[{uuid}]: '
|
||||||
'type={map_type} {key}={value}>').format(
|
'type={map_type} {value}={cost}>').format(
|
||||||
id=self.id,
|
uuid=self.mapping_id,
|
||||||
map_type=self.map_type,
|
map_type=self.map_type,
|
||||||
key=self.key,
|
value=self.value,
|
||||||
value=self.value)
|
cost=self.cost)
|
||||||
|
|
|
@ -16,13 +16,14 @@
|
||||||
# @author: Gauvain Pocentek
|
# @author: Gauvain Pocentek
|
||||||
#
|
#
|
||||||
from oslo.config import fixture as config_fixture
|
from oslo.config import fixture as config_fixture
|
||||||
|
from oslotest import base
|
||||||
import testscenarios
|
import testscenarios
|
||||||
import testtools
|
|
||||||
|
|
||||||
|
from cloudkitty import db
|
||||||
from cloudkitty.db import api as db_api
|
from cloudkitty.db import api as db_api
|
||||||
|
|
||||||
|
|
||||||
class TestCase(testscenarios.TestWithScenarios, testtools.TestCase):
|
class TestCase(testscenarios.TestWithScenarios, base.BaseTestCase):
|
||||||
scenarios = [
|
scenarios = [
|
||||||
('sqlite', dict(db_url='sqlite:///'))
|
('sqlite', dict(db_url='sqlite:///'))
|
||||||
]
|
]
|
||||||
|
@ -34,3 +35,7 @@ class TestCase(testscenarios.TestWithScenarios, testtools.TestCase):
|
||||||
self.conn = db_api.get_instance()
|
self.conn = db_api.get_instance()
|
||||||
migration = self.conn.get_migration()
|
migration = self.conn.get_migration()
|
||||||
migration.upgrade('head')
|
migration.upgrade('head')
|
||||||
|
|
||||||
|
def tearDown(self):
|
||||||
|
db.get_engine().dispose()
|
||||||
|
super(TestCase, self).tearDown()
|
||||||
|
|
|
@ -0,0 +1,520 @@
|
||||||
|
# -*- 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
|
||||||
|
|
||||||
|
import mock
|
||||||
|
from oslo.utils import uuidutils
|
||||||
|
|
||||||
|
from cloudkitty.billing import hash
|
||||||
|
from cloudkitty.billing.hash.db import api
|
||||||
|
from cloudkitty import tests
|
||||||
|
|
||||||
|
TEST_TS = 1388577600
|
||||||
|
FAKE_UUID = '6c1b8a30-797f-4b7e-ad66-9879b79059fb'
|
||||||
|
CK_RESOURCES_DATA = [{
|
||||||
|
"period": {
|
||||||
|
"begin": "2014-10-01T00:00:00",
|
||||||
|
"end": "2014-10-01T01:00:00"},
|
||||||
|
"usage": {
|
||||||
|
"compute": [
|
||||||
|
{
|
||||||
|
"desc": {
|
||||||
|
"availability_zone": "nova",
|
||||||
|
"flavor": "m1.nano",
|
||||||
|
"image_id": "f5600101-8fa2-4864-899e-ebcb7ed6b568",
|
||||||
|
"memory": "64",
|
||||||
|
"metadata": {
|
||||||
|
"farm": "prod"},
|
||||||
|
"name": "prod1",
|
||||||
|
"project_id": "f266f30b11f246b589fd266f85eeec39",
|
||||||
|
"user_id": "55b3379b949243009ee96972fbf51ed1",
|
||||||
|
"vcpus": "1"},
|
||||||
|
"vol": {
|
||||||
|
"qty": 1,
|
||||||
|
"unit": "instance"}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"desc": {
|
||||||
|
"availability_zone": "nova",
|
||||||
|
"flavor": "m1.tiny",
|
||||||
|
"image_id": "a41fba37-2429-4f15-aa00-b5bc4bf557bf",
|
||||||
|
"memory": "512",
|
||||||
|
"metadata": {
|
||||||
|
"farm": "dev"},
|
||||||
|
"name": "dev1",
|
||||||
|
"project_id": "f266f30b11f246b589fd266f85eeec39",
|
||||||
|
"user_id": "55b3379b949243009ee96972fbf51ed1",
|
||||||
|
"vcpus": "1"},
|
||||||
|
"vol": {
|
||||||
|
"qty": 1,
|
||||||
|
"unit": "instance"}},
|
||||||
|
{
|
||||||
|
"desc": {
|
||||||
|
"availability_zone": "nova",
|
||||||
|
"flavor": "m1.nano",
|
||||||
|
"image_id": "a41fba37-2429-4f15-aa00-b5bc4bf557bf",
|
||||||
|
"memory": "64",
|
||||||
|
"metadata": {
|
||||||
|
"farm": "dev"},
|
||||||
|
"name": "dev2",
|
||||||
|
"project_id": "f266f30b11f246b589fd266f85eeec39",
|
||||||
|
"user_id": "55b3379b949243009ee96972fbf51ed1",
|
||||||
|
"vcpus": "1"},
|
||||||
|
"vol": {
|
||||||
|
"qty": 1,
|
||||||
|
"unit": "instance"}}]}}]
|
||||||
|
|
||||||
|
|
||||||
|
class HashMapRatingTest(tests.TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
super(HashMapRatingTest, self).setUp()
|
||||||
|
self._tenant_id = 'f266f30b11f246b589fd266f85eeec39'
|
||||||
|
self._db_api = hash.HashMap.db_api
|
||||||
|
self._db_api.get_migration().upgrade('head')
|
||||||
|
self._hash = hash.HashMap(self._tenant_id)
|
||||||
|
|
||||||
|
# Group tests
|
||||||
|
@mock.patch.object(uuidutils, 'generate_uuid',
|
||||||
|
return_value=FAKE_UUID)
|
||||||
|
def test_create_group(self, patch_generate_uuid):
|
||||||
|
self._db_api.create_group('test_group')
|
||||||
|
groups = self._db_api.list_groups()
|
||||||
|
self.assertEqual([FAKE_UUID], groups)
|
||||||
|
patch_generate_uuid.assert_called_once()
|
||||||
|
|
||||||
|
def test_create_duplicate_group(self):
|
||||||
|
self._db_api.create_group('test_group')
|
||||||
|
self.assertRaises(api.GroupAlreadyExists,
|
||||||
|
self._db_api.create_group,
|
||||||
|
'test_group')
|
||||||
|
|
||||||
|
def test_delete_group(self):
|
||||||
|
group_db = self._db_api.create_group('test_group')
|
||||||
|
self._db_api.delete_group(group_db.group_id)
|
||||||
|
groups = self._db_api.list_groups()
|
||||||
|
self.assertEqual([], groups)
|
||||||
|
|
||||||
|
def test_delete_unknown_group(self):
|
||||||
|
self.assertRaises(api.NoSuchGroup,
|
||||||
|
self._db_api.delete_group,
|
||||||
|
uuidutils.generate_uuid())
|
||||||
|
|
||||||
|
def test_recursive_delete_group(self):
|
||||||
|
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(
|
||||||
|
value='m1.tiny',
|
||||||
|
cost='1.337',
|
||||||
|
map_type='flat',
|
||||||
|
field_id=field_db.field_id,
|
||||||
|
group_id=group_db.group_id)
|
||||||
|
self._db_api.delete_group(group_db.group_id)
|
||||||
|
mappings = self._db_api.list_mappings(field_uuid=field_db.field_id)
|
||||||
|
self.assertEqual([], mappings)
|
||||||
|
groups = self._db_api.list_groups()
|
||||||
|
self.assertEqual([], groups)
|
||||||
|
|
||||||
|
def test_non_recursive_delete_group(self):
|
||||||
|
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_db = self._db_api.create_mapping(
|
||||||
|
value='m1.tiny',
|
||||||
|
cost='1.337',
|
||||||
|
map_type='flat',
|
||||||
|
field_id=field_db.field_id,
|
||||||
|
group_id=group_db.group_id)
|
||||||
|
self._db_api.delete_group(group_db.group_id, False)
|
||||||
|
mappings = self._db_api.list_mappings(field_uuid=field_db.field_id)
|
||||||
|
self.assertEqual([mapping_db.mapping_id], mappings)
|
||||||
|
groups = self._db_api.list_groups()
|
||||||
|
self.assertEqual([], groups)
|
||||||
|
new_mapping_db = self._db_api.get_mapping(mapping_db.mapping_id)
|
||||||
|
self.assertEqual(None, new_mapping_db.group_id)
|
||||||
|
|
||||||
|
def test_list_mappings_from_group(self):
|
||||||
|
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_tiny = self._db_api.create_mapping(
|
||||||
|
value='m1.tiny',
|
||||||
|
cost='1.337',
|
||||||
|
map_type='flat',
|
||||||
|
field_id=field_db.field_id,
|
||||||
|
group_id=group_db.group_id)
|
||||||
|
mapping_small = self._db_api.create_mapping(
|
||||||
|
value='m1.small',
|
||||||
|
cost='3.1337',
|
||||||
|
map_type='flat',
|
||||||
|
field_id=field_db.field_id,
|
||||||
|
group_id=group_db.group_id)
|
||||||
|
self._db_api.create_mapping(
|
||||||
|
value='m1.large',
|
||||||
|
cost='42',
|
||||||
|
map_type='flat',
|
||||||
|
field_id=field_db.field_id)
|
||||||
|
mappings = self._db_api.list_mappings(field_uuid=field_db.field_id,
|
||||||
|
group_uuid=group_db.group_id)
|
||||||
|
self.assertEqual([mapping_tiny.mapping_id,
|
||||||
|
mapping_small.mapping_id],
|
||||||
|
mappings)
|
||||||
|
|
||||||
|
def test_list_mappings_without_group(self):
|
||||||
|
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(
|
||||||
|
value='m1.tiny',
|
||||||
|
cost='1.337',
|
||||||
|
map_type='flat',
|
||||||
|
field_id=field_db.field_id,
|
||||||
|
group_id=group_db.group_id)
|
||||||
|
self._db_api.create_mapping(
|
||||||
|
value='m1.small',
|
||||||
|
cost='3.1337',
|
||||||
|
map_type='flat',
|
||||||
|
field_id=field_db.field_id,
|
||||||
|
group_id=group_db.group_id)
|
||||||
|
mapping_no_group = self._db_api.create_mapping(
|
||||||
|
value='m1.large',
|
||||||
|
cost='42',
|
||||||
|
map_type='flat',
|
||||||
|
field_id=field_db.field_id)
|
||||||
|
mappings = self._db_api.list_mappings(field_uuid=field_db.field_id,
|
||||||
|
no_group=True)
|
||||||
|
self.assertEqual([mapping_no_group.mapping_id],
|
||||||
|
mappings)
|
||||||
|
|
||||||
|
# Service tests
|
||||||
|
@mock.patch.object(uuidutils, 'generate_uuid',
|
||||||
|
return_value=FAKE_UUID)
|
||||||
|
def test_create_service(self, patch_generate_uuid):
|
||||||
|
self._db_api.create_service('compute')
|
||||||
|
services = self._db_api.list_services()
|
||||||
|
self.assertEqual([FAKE_UUID], services)
|
||||||
|
patch_generate_uuid.assert_called_once()
|
||||||
|
|
||||||
|
def test_create_duplicate_service(self):
|
||||||
|
self._db_api.create_service('compute')
|
||||||
|
self.assertRaises(api.ServiceAlreadyExists,
|
||||||
|
self._db_api.create_service,
|
||||||
|
'compute')
|
||||||
|
|
||||||
|
def test_delete_service_by_name(self):
|
||||||
|
self._db_api.create_service('compute')
|
||||||
|
self._db_api.delete_service('compute')
|
||||||
|
services = self._db_api.list_services()
|
||||||
|
self.assertEqual([], services)
|
||||||
|
|
||||||
|
def test_delete_service_by_uuid(self):
|
||||||
|
service_db = self._db_api.create_service('compute')
|
||||||
|
self._db_api.delete_service(uuid=service_db.service_id)
|
||||||
|
services = self._db_api.list_services()
|
||||||
|
self.assertEqual([], services)
|
||||||
|
|
||||||
|
def test_delete_unknown_service_by_name(self):
|
||||||
|
self.assertRaises(api.NoSuchService,
|
||||||
|
self._db_api.delete_service,
|
||||||
|
'dummy')
|
||||||
|
|
||||||
|
def test_delete_unknown_service_by_uuid(self):
|
||||||
|
self.assertRaises(
|
||||||
|
api.NoSuchService,
|
||||||
|
self._db_api.delete_service,
|
||||||
|
uuid='6e8de9fc-ee17-4b60-b81a-c9320e994e76')
|
||||||
|
|
||||||
|
# Field tests
|
||||||
|
def test_create_field_in_existing_service(self):
|
||||||
|
service_db = self._db_api.create_service('compute')
|
||||||
|
field_db = self._db_api.create_field(service_db.service_id,
|
||||||
|
'flavor')
|
||||||
|
fields = self._db_api.list_fields(service_db.service_id)
|
||||||
|
self.assertEqual([field_db.field_id], fields)
|
||||||
|
|
||||||
|
def test_create_duplicate_field(self):
|
||||||
|
service_db = self._db_api.create_service('compute')
|
||||||
|
self._db_api.create_field(service_db.service_id,
|
||||||
|
'flavor')
|
||||||
|
self.assertRaises(api.FieldAlreadyExists,
|
||||||
|
self._db_api.create_field,
|
||||||
|
service_db.service_id,
|
||||||
|
'flavor')
|
||||||
|
|
||||||
|
def test_delete_field(self):
|
||||||
|
service_db = self._db_api.create_service('compute')
|
||||||
|
field_db = self._db_api.create_field(service_db.service_id, 'flavor')
|
||||||
|
self._db_api.delete_field(field_db.field_id)
|
||||||
|
services = self._db_api.list_services()
|
||||||
|
self.assertEqual([service_db.service_id], services)
|
||||||
|
fields = self._db_api.list_fields(service_db.service_id)
|
||||||
|
self.assertEqual([], fields)
|
||||||
|
|
||||||
|
def test_delete_unknown_field(self):
|
||||||
|
self.assertRaises(api.NoSuchField,
|
||||||
|
self._db_api.delete_field,
|
||||||
|
uuidutils.generate_uuid())
|
||||||
|
|
||||||
|
def test_recursive_delete_field_from_service(self):
|
||||||
|
service_db = self._db_api.create_service('compute')
|
||||||
|
field_db = self._db_api.create_field(service_db.service_id,
|
||||||
|
'flavor')
|
||||||
|
self._db_api.delete_service(uuid=service_db.service_id)
|
||||||
|
self.assertRaises(api.NoSuchField,
|
||||||
|
self._db_api.get_field,
|
||||||
|
field_db.field_id)
|
||||||
|
|
||||||
|
# Mapping tests
|
||||||
|
def test_create_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)
|
||||||
|
mappings = self._db_api.list_mappings(field_uuid=field_db.field_id)
|
||||||
|
self.assertEqual([mapping_db.mapping_id], mappings)
|
||||||
|
|
||||||
|
def test_list_mappings_from_services(self):
|
||||||
|
service_db = self._db_api.create_service('compute')
|
||||||
|
mapping_db = self._db_api.create_mapping(
|
||||||
|
cost='1.337',
|
||||||
|
map_type='flat',
|
||||||
|
service_id=service_db.service_id)
|
||||||
|
mappings = self._db_api.list_mappings(
|
||||||
|
service_uuid=service_db.service_id)
|
||||||
|
self.assertEqual([mapping_db.mapping_id], mappings)
|
||||||
|
|
||||||
|
def test_list_mappings_from_fields(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)
|
||||||
|
mappings = self._db_api.list_mappings(
|
||||||
|
field_uuid=field_db.field_id)
|
||||||
|
self.assertEqual([mapping_db.mapping_id], mappings)
|
||||||
|
|
||||||
|
def test_create_mapping_with_incorrect_type(self):
|
||||||
|
service_db = self._db_api.create_service('compute')
|
||||||
|
field_db = self._db_api.create_field(service_db.service_id,
|
||||||
|
'flavor')
|
||||||
|
self.assertRaises(api.NoSuchType,
|
||||||
|
self._db_api.create_mapping,
|
||||||
|
value='m1.tiny',
|
||||||
|
cost='1.337',
|
||||||
|
map_type='invalid',
|
||||||
|
field_id=field_db.field_id)
|
||||||
|
|
||||||
|
def test_create_mapping_with_two_parents(self):
|
||||||
|
service_db = self._db_api.create_service('compute')
|
||||||
|
field_db = self._db_api.create_field(service_db.service_id,
|
||||||
|
'flavor')
|
||||||
|
self.assertRaises(ValueError,
|
||||||
|
self._db_api.create_mapping,
|
||||||
|
value='m1.tiny',
|
||||||
|
cost='1.337',
|
||||||
|
map_type='flat',
|
||||||
|
service_id=service_db.service_id,
|
||||||
|
field_id=field_db.field_id)
|
||||||
|
|
||||||
|
def test_update_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)
|
||||||
|
new_mapping_db = self._db_api.update_mapping(
|
||||||
|
uuid=mapping_db.mapping_id,
|
||||||
|
value='42',
|
||||||
|
map_type='rate')
|
||||||
|
self.assertEqual('42', new_mapping_db.value)
|
||||||
|
self.assertEqual('rate', new_mapping_db.map_type)
|
||||||
|
|
||||||
|
def test_update_mapping_inside_group(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)
|
||||||
|
group_db = self._db_api.create_group('test_group')
|
||||||
|
new_mapping_db = self._db_api.update_mapping(
|
||||||
|
mapping_db.mapping_id,
|
||||||
|
value='42',
|
||||||
|
map_type='rate',
|
||||||
|
group_id=group_db.group_id)
|
||||||
|
self.assertEqual('42', new_mapping_db.value)
|
||||||
|
self.assertEqual('rate', new_mapping_db.map_type)
|
||||||
|
self.assertEqual(group_db.id, new_mapping_db.group_id)
|
||||||
|
|
||||||
|
def test_delete_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)
|
||||||
|
self._db_api.delete_mapping(mapping_db.mapping_id)
|
||||||
|
mappings = self._db_api.list_mappings(field_uuid=field_db.field_id)
|
||||||
|
self.assertEqual([], mappings)
|
||||||
|
|
||||||
|
# Processing tests
|
||||||
|
def test_load_billing_rates(self):
|
||||||
|
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'),
|
||||||
|
'type': 'rate'}}}
|
||||||
|
|
||||||
|
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_process_service_map(self):
|
||||||
|
service_db = self._db_api.create_service('compute')
|
||||||
|
group_db = self._db_api.create_group('test_group')
|
||||||
|
self._db_api.create_mapping(
|
||||||
|
cost='1.337',
|
||||||
|
map_type='flat',
|
||||||
|
service_id=service_db.service_id,
|
||||||
|
group_id=group_db.group_id)
|
||||||
|
self._db_api.create_mapping(
|
||||||
|
cost='1.42',
|
||||||
|
map_type='flat',
|
||||||
|
service_id=service_db.service_id)
|
||||||
|
self._hash.reload_config()
|
||||||
|
actual_data = CK_RESOURCES_DATA[:]
|
||||||
|
expected_data = CK_RESOURCES_DATA[:]
|
||||||
|
compute_list = expected_data[0]['usage']['compute']
|
||||||
|
compute_list[0]['billing'] = {'price': decimal.Decimal('2.757')}
|
||||||
|
compute_list[1]['billing'] = {'price': decimal.Decimal('2.757')}
|
||||||
|
compute_list[2]['billing'] = {'price': decimal.Decimal('2.757')}
|
||||||
|
self._hash.process(actual_data)
|
||||||
|
self.assertEqual(expected_data, actual_data)
|
||||||
|
|
||||||
|
def test_process_field_map(self):
|
||||||
|
service_db = self._db_api.create_service('compute')
|
||||||
|
flavor_field = self._db_api.create_field(service_db.service_id,
|
||||||
|
'flavor')
|
||||||
|
image_field = self._db_api.create_field(service_db.service_id,
|
||||||
|
'image_id')
|
||||||
|
group_db = self._db_api.create_group('test_group')
|
||||||
|
self._db_api.create_mapping(
|
||||||
|
value='m1.nano',
|
||||||
|
cost='1.337',
|
||||||
|
map_type='flat',
|
||||||
|
field_id=flavor_field.field_id,
|
||||||
|
group_id=group_db.group_id)
|
||||||
|
self._db_api.create_mapping(
|
||||||
|
value='a41fba37-2429-4f15-aa00-b5bc4bf557bf',
|
||||||
|
cost='1.10',
|
||||||
|
map_type='rate',
|
||||||
|
field_id=image_field.field_id,
|
||||||
|
group_id=group_db.group_id)
|
||||||
|
self._db_api.create_mapping(
|
||||||
|
value='m1.tiny',
|
||||||
|
cost='1.42',
|
||||||
|
map_type='flat',
|
||||||
|
field_id=flavor_field.field_id)
|
||||||
|
self._hash.reload_config()
|
||||||
|
actual_data = CK_RESOURCES_DATA[:]
|
||||||
|
expected_data = CK_RESOURCES_DATA[:]
|
||||||
|
compute_list = expected_data[0]['usage']['compute']
|
||||||
|
compute_list[0]['billing'] = {'price': decimal.Decimal('1.337')}
|
||||||
|
compute_list[1]['billing'] = {'price': decimal.Decimal('1.42')}
|
||||||
|
compute_list[2]['billing'] = {'price': decimal.Decimal('1.47070')}
|
||||||
|
self._hash.process(actual_data)
|
||||||
|
self.assertEqual(expected_data, actual_data)
|
||||||
|
|
||||||
|
def test_process_billing(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')
|
||||||
|
self._db_api.create_mapping(
|
||||||
|
cost='1.00',
|
||||||
|
map_type='flat',
|
||||||
|
service_id=service_db.service_id)
|
||||||
|
self._db_api.create_mapping(
|
||||||
|
value='m1.nano',
|
||||||
|
cost='1.337',
|
||||||
|
map_type='flat',
|
||||||
|
field_id=field_db.field_id)
|
||||||
|
self._db_api.create_mapping(
|
||||||
|
value='m1.tiny',
|
||||||
|
cost='1.42',
|
||||||
|
map_type='flat',
|
||||||
|
field_id=field_db.field_id)
|
||||||
|
self._hash.reload_config()
|
||||||
|
actual_data = CK_RESOURCES_DATA[:]
|
||||||
|
expected_data = CK_RESOURCES_DATA[:]
|
||||||
|
compute_list = expected_data[0]['usage']['compute']
|
||||||
|
compute_list[0]['billing'] = {'price': decimal.Decimal('1.337')}
|
||||||
|
compute_list[1]['billing'] = {'price': decimal.Decimal('1.42')}
|
||||||
|
compute_list[2]['billing'] = {'price': decimal.Decimal('1.42')}
|
||||||
|
self._hash.process(actual_data)
|
||||||
|
self.assertEqual(expected_data, actual_data)
|
|
@ -2,26 +2,41 @@
|
||||||
HashMap Module REST API
|
HashMap Module REST API
|
||||||
=======================
|
=======================
|
||||||
|
|
||||||
.. rest-controller:: cloudkitty.billing.hash:BasicHashMapConfigController
|
.. rest-controller:: cloudkitty.billing.hash.controllers.root:HashMapConfigController
|
||||||
:webprefix: /v1/billing/module_config/hashmap
|
:webprefix: /v1/billing/module_config/hashmap
|
||||||
|
|
||||||
.. http:get:: /v1/billing/hashmap/modules/config/(service)/(field)/(key)
|
.. rest-controller:: cloudkitty.billing.hash.controllers.service:HashMapServicesController
|
||||||
|
:webprefix: /v1/billing/module_config/hashmap/services
|
||||||
|
|
||||||
Get a mapping from full path
|
.. autotype:: cloudkitty.billing.hash.datamodels.service.Service
|
||||||
|
|
||||||
:param service: Filter on this service.
|
|
||||||
:param field: Filter on this field.
|
|
||||||
:param key: Filter on this key.
|
|
||||||
:type service: :class:`unicode`
|
|
||||||
:type field: :class:`unicode`
|
|
||||||
:type key: :class:`unicode`
|
|
||||||
:type mapping: :class:`Mapping`
|
|
||||||
:return: A mapping
|
|
||||||
|
|
||||||
:return type: :class:`Mapping`
|
|
||||||
|
|
||||||
|
|
||||||
.. autotype:: cloudkitty.billing.hash.Mapping
|
|
||||||
:members:
|
:members:
|
||||||
|
|
||||||
|
.. autotype:: cloudkitty.billing.hash.datamodels.service.ServiceCollection
|
||||||
|
:members:
|
||||||
|
|
||||||
|
.. rest-controller:: cloudkitty.billing.hash.controllers.field:HashMapFieldsController
|
||||||
|
:webprefix: /v1/billing/module_config/hashmap/fields
|
||||||
|
|
||||||
|
.. autotype:: cloudkitty.billing.hash.datamodels.field.Field
|
||||||
|
:members:
|
||||||
|
|
||||||
|
.. autotype:: cloudkitty.billing.hash.datamodels.field.FieldCollection
|
||||||
|
:members:
|
||||||
|
|
||||||
|
.. rest-controller:: cloudkitty.billing.hash.controllers.mapping:HashMapMappingsController
|
||||||
|
:webprefix: /v1/billing/module_config/hashmap/mappings
|
||||||
|
|
||||||
|
.. autotype:: cloudkitty.billing.hash.datamodels.mapping.Mapping
|
||||||
|
:members:
|
||||||
|
|
||||||
|
.. autotype:: cloudkitty.billing.hash.datamodels.mapping.MappingCollection
|
||||||
|
:members:
|
||||||
|
|
||||||
|
.. rest-controller:: cloudkitty.billing.hash.controllers.group:HashMapGroupsController
|
||||||
|
:webprefix: /v1/billing/module_config/hashmap/groups
|
||||||
|
|
||||||
|
.. autotype:: cloudkitty.billing.hash.datamodels.group.Group
|
||||||
|
:members:
|
||||||
|
|
||||||
|
.. autotype:: cloudkitty.billing.hash.datamodels.group.GroupCollection
|
||||||
|
:members:
|
||||||
|
|
|
@ -39,7 +39,7 @@ cloudkitty.transformers =
|
||||||
|
|
||||||
cloudkitty.billing.processors =
|
cloudkitty.billing.processors =
|
||||||
noop = cloudkitty.billing.noop:Noop
|
noop = cloudkitty.billing.noop:Noop
|
||||||
hashmap = cloudkitty.billing.hash:BasicHashMap
|
hashmap = cloudkitty.billing.hash:HashMap
|
||||||
|
|
||||||
cloudkitty.storage.backends =
|
cloudkitty.storage.backends =
|
||||||
sqlalchemy = cloudkitty.storage.sqlalchemy:SQLAlchemyStorage
|
sqlalchemy = cloudkitty.storage.sqlalchemy:SQLAlchemyStorage
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
hacking>=0.9.2,<0.10
|
hacking>=0.9.2,<0.10
|
||||||
coverage>=3.6
|
coverage>=3.6
|
||||||
discover
|
discover
|
||||||
testtools
|
|
||||||
testscenarios
|
testscenarios
|
||||||
testrepository
|
testrepository
|
||||||
mock>=1.0
|
mock>=1.0
|
||||||
|
|
Loading…
Reference in New Issue