Renaming billing to rating

As CloudKitty is a rating and not a billing component, we needed to
rename all components and change paths on the API.
We've created a compatibility layer to help people with the migration.
The old API objects are now deprecated and shouldn't be used, they will
be removed in a near future.

Change-Id: I9e264f4ed01f4c94366eb51f612d634312b6a2f7
This commit is contained in:
Stéphane Albert 2015-03-17 16:00:22 +01:00
parent ab2a6bff5a
commit 229043fc42
68 changed files with 2564 additions and 2070 deletions

View File

@ -2,14 +2,14 @@
CloudKitty
==========
OpenStack Billing and Usage Reporter
++++++++++++++++++++++++++++++++++++
OpenStack Rating and Usage Reporter
+++++++++++++++++++++++++++++++++++
Goal
----
The goal of this project is to automate the extraction of the metrics from
ceilometer, map them to billing informations and generate reports.
ceilometer, map them to rating informations and generate reports.
Status
------
@ -22,11 +22,11 @@ time between commits can be long.
Roadmap
-------
* Create a project API to manage the configuration of billing modules and
* Create a project API to manage the configuration of rating modules and
request informations.
* Every billing module should be able to expose its own API.
* Every rating module should be able to expose its own API.
* Move from importutils to stevedore.
* Scheduling of billing calculations
* Scheduling of rating calculations
* Better collection of ceilometer metrics (Maybe Gnocchi)
* Global code improvement

View File

@ -17,8 +17,8 @@
#
from pecan import rest
from cloudkitty.api.v1.controllers import billing as billing_api
from cloudkitty.api.v1.controllers import collector as collector_api
from cloudkitty.api.v1.controllers import rating as rating_api
from cloudkitty.api.v1.controllers import report as report_api
from cloudkitty.api.v1.controllers import storage as storage_api
@ -28,7 +28,8 @@ class V1Controller(rest.RestController):
"""
billing = billing_api.BillingController()
billing = rating_api.RatingController()
collector = collector_api.CollectorController()
rating = rating_api.RatingController()
report = report_api.ReportController()
storage = storage_api.StorageController()

View File

@ -15,150 +15,30 @@
#
# @author: Stéphane Albert
#
from oslo.config import cfg
import pecan
from pecan import rest
from stevedore import extension
from wsme import types as wtypes
import wsmeext.pecan as wsme_pecan
import warnings
from cloudkitty.api.v1.datamodels import billing as billing_models
from cloudkitty import config # noqa
from cloudkitty.api.v1.controllers import rating as rating_api
from cloudkitty.api.v1.controllers.rating import ModulesController # noqa
from cloudkitty.api.v1.controllers.rating import ModulesExposer # noqa
from cloudkitty.api.v1.controllers.rating import UnconfigurableController # noqa
from cloudkitty.openstack.common import log as logging
CONF = cfg.CONF
LOG = logging.getLogger(__name__)
class ModulesController(rest.RestController):
"""REST Controller managing billing modules."""
def __init__(self):
self.extensions = extension.ExtensionManager(
'cloudkitty.billing.processors',
# FIXME(sheeprine): don't want to load it here as we just need the
# controller
invoke_on_load=True
)
@wsme_pecan.wsexpose(billing_models.CloudkittyModuleCollection)
def get_all(self):
"""return the list of loaded modules.
:return: name of every loaded modules.
"""
modules_list = []
for module in self.extensions:
infos = module.obj.module_info.copy()
infos['module_id'] = infos.pop('name')
modules_list.append(billing_models.CloudkittyModule(**infos))
return billing_models.CloudkittyModuleCollection(
modules=modules_list
)
@wsme_pecan.wsexpose(billing_models.CloudkittyModule, wtypes.text)
def get_one(self, module_id):
"""return a module
:return: CloudKittyModule
"""
try:
module = self.extensions[module_id]
except KeyError:
pecan.abort(404)
infos = module.obj.module_info.copy()
infos['module_id'] = infos.pop('name')
return billing_models.CloudkittyModule(**infos)
@wsme_pecan.wsexpose(billing_models.CloudkittyModule,
wtypes.text,
body=billing_models.CloudkittyModule,
status_code=302)
def put(self, module_id, module):
"""Change the state of a module (enabled/disabled)
:param module_id: name of the module to modify
:param module: CloudKittyModule object describing the new desired state
## :return: CloudKittyModule object describing the desired state
"""
try:
self.extensions[module_id].obj.set_state(module.enabled)
except KeyError:
pecan.abort(404)
pecan.response.location = pecan.request.path
def deprecated():
warnings.warn(
('The billing controllers are deprecated. '
'Please use rating\'s one instead.'),
DeprecationWarning,
stacklevel=3)
class UnconfigurableController(rest.RestController):
"""This controller raises an error when requested."""
@wsme_pecan.wsexpose(None)
def put(self):
self.abort()
@wsme_pecan.wsexpose(None)
def get(self):
self.abort()
def abort(self):
pecan.abort(409, "Module is not configurable")
deprecated()
class ModulesExposer(rest.RestController):
"""REST Controller exposing billing modules.
This is the controller that exposes the modules own configuration
settings.
"""
def __init__(self):
self.extensions = extension.ExtensionManager(
'cloudkitty.billing.processors',
# FIXME(sheeprine): don't want to load it here as we just need the
# controller
invoke_on_load=True
)
self.expose_modules()
def expose_modules(self):
"""Load billing modules to expose API controllers."""
for ext in self.extensions:
# FIXME(sheeprine): we should notify two modules with same name
if not hasattr(self, ext.name):
if not ext.obj.config_controller:
ext.obj.config_controller = UnconfigurableController
setattr(self, ext.name, ext.obj.config_controller())
class BillingController(rest.RestController):
class BillingController(rating_api.RatingController):
"""The BillingController is exposed by the API.
The BillingControler connects the ModulesExposer, ModulesController
and a quote action to the API.
Deprecated, replaced by the RatingController.
"""
_custom_actions = {
'quote': ['POST'],
}
modules = ModulesController()
module_config = ModulesExposer()
@wsme_pecan.wsexpose(float,
body=billing_models.CloudkittyResourceCollection)
def quote(self, res_data):
"""Get an instant quote based on multiple resource descriptions.
:param res_data: List of resource descriptions.
:return: Total price for these descriptions.
"""
client = pecan.request.rpc_client.prepare(namespace='billing')
res_dict = {}
for res in res_data.resources:
if res.service not in res_dict:
res_dict[res.service] = []
json_data = res.to_json()
res_dict[res.service].extend(json_data[res.service])
res = client.call({}, 'quote', res_data=[{'usage': res_dict}])
return res

View File

@ -0,0 +1,163 @@
# -*- coding: utf-8 -*-
# Copyright 2014 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
from stevedore import extension
from wsme import types as wtypes
import wsmeext.pecan as wsme_pecan
from cloudkitty.api.v1.datamodels import rating as rating_models
from cloudkitty.openstack.common import log as logging
LOG = logging.getLogger(__name__)
PROCESSORS_NAMESPACE = 'cloudkitty.rating.processors'
class ModulesController(rest.RestController):
"""REST Controller managing rating modules."""
def __init__(self):
self.extensions = extension.ExtensionManager(
PROCESSORS_NAMESPACE,
# FIXME(sheeprine): don't want to load it here as we just need the
# controller
invoke_on_load=True
)
@wsme_pecan.wsexpose(rating_models.CloudkittyModuleCollection)
def get_all(self):
"""return the list of loaded modules.
:return: name of every loaded modules.
"""
modules_list = []
for module in self.extensions:
infos = module.obj.module_info.copy()
infos['module_id'] = infos.pop('name')
modules_list.append(rating_models.CloudkittyModule(**infos))
return rating_models.CloudkittyModuleCollection(
modules=modules_list
)
@wsme_pecan.wsexpose(rating_models.CloudkittyModule, wtypes.text)
def get_one(self, module_id):
"""return a module
:return: CloudKittyModule
"""
try:
module = self.extensions[module_id]
except KeyError:
pecan.abort(404)
infos = module.obj.module_info.copy()
infos['module_id'] = infos.pop('name')
return rating_models.CloudkittyModule(**infos)
@wsme_pecan.wsexpose(rating_models.CloudkittyModule,
wtypes.text,
body=rating_models.CloudkittyModule,
status_code=302)
def put(self, module_id, module):
"""Change the state of a module (enabled/disabled)
:param module_id: name of the module to modify
:param module: CloudKittyModule object describing the new desired state
## :return: CloudKittyModule object describing the desired state
"""
try:
self.extensions[module_id].obj.set_state(module.enabled)
except KeyError:
pecan.abort(404)
pecan.response.location = pecan.request.path
class UnconfigurableController(rest.RestController):
"""This controller raises an error when requested."""
@wsme_pecan.wsexpose(None)
def put(self):
self.abort()
@wsme_pecan.wsexpose(None)
def get(self):
self.abort()
def abort(self):
pecan.abort(409, "Module is not configurable")
class ModulesExposer(rest.RestController):
"""REST Controller exposing rating modules.
This is the controller that exposes the modules own configuration
settings.
"""
def __init__(self):
self.extensions = extension.ExtensionManager(
PROCESSORS_NAMESPACE,
# FIXME(sheeprine): don't want to load it here as we just need the
# controller
invoke_on_load=True
)
self.expose_modules()
def expose_modules(self):
"""Load rating modules to expose API controllers."""
for ext in self.extensions:
# FIXME(sheeprine): we should notify two modules with same name
if not hasattr(self, ext.name):
if not ext.obj.config_controller:
ext.obj.config_controller = UnconfigurableController
setattr(self, ext.name, ext.obj.config_controller())
class RatingController(rest.RestController):
"""The RatingController is exposed by the API.
The RatingControler connects the ModulesExposer, ModulesController
and a quote action to the API.
"""
_custom_actions = {
'quote': ['POST'],
}
modules = ModulesController()
module_config = ModulesExposer()
@wsme_pecan.wsexpose(float,
body=rating_models.CloudkittyResourceCollection)
def quote(self, res_data):
"""Get an instant quote based on multiple resource descriptions.
:param res_data: List of resource descriptions.
:return: Total price for these descriptions.
"""
client = pecan.request.rpc_client.prepare(namespace='rating')
res_dict = {}
for res in res_data.resources:
if res.service not in res_dict:
res_dict[res.service] = []
json_data = res.to_json()
res_dict[res.service].extend(json_data[res.service])
res = client.call({}, 'quote', res_data=[{'usage': res_dict}])
return res

View File

@ -62,12 +62,12 @@ class StorageController(rest.RestController):
resources = []
for data in data_list:
desc = data['desc'] if data['desc'] else {}
price = decimal.Decimal(data['billing']['price'])
price = decimal.Decimal(data['rating']['price'])
resource = storage_models.RatedResource(
service=service,
desc=desc,
volume=data['vol']['qty'],
billing=price)
rating=price)
resources.append(resource)
data_frame = storage_models.DataFrame(
begin=ck_utils.iso2dt(frame['period']['begin']),

View File

@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
# Copyright 2014 Objectif Libre
#
@ -16,87 +15,17 @@
#
# @author: Stéphane Albert
#
import decimal
import warnings
from oslo.config import cfg
from wsme import types as wtypes
from cloudkitty.api.v1 import types as cktypes
from cloudkitty import config # noqa
CONF = cfg.CONF
CONF.import_opt('services', 'cloudkitty.collector', 'collect')
CLOUDKITTY_SERVICES = wtypes.Enum(wtypes.text,
*CONF.collect.services)
from cloudkitty.api.v1.datamodels.rating import * # noqa
class CloudkittyResource(wtypes.Base):
"""Type describing a resource in CloudKitty.
"""
service = CLOUDKITTY_SERVICES
"""Name of the service."""
# FIXME(sheeprine): values should be dynamic
# Testing with ironic dynamic type
desc = {wtypes.text: cktypes.MultiType(wtypes.text, int, float, dict)}
"""Description of the resources parameters."""
volume = decimal.Decimal
"""Volume of resources."""
def to_json(self):
res_dict = {}
res_dict[self.service] = [{'desc': self.desc,
'vol': {'qty': self.volume,
'unit': 'undef'}
}]
return res_dict
@classmethod
def sample(cls):
sample = cls(service='compute',
desc={
'image_id': 'a41fba37-2429-4f15-aa00-b5bc4bf557bf'
},
volume=decimal.Decimal(1))
return sample
def deprecated():
warnings.warn(
('The billing datamodels are deprecated. '
'Please use rating\'s one instead.'),
DeprecationWarning,
stacklevel=3)
class CloudkittyResourceCollection(wtypes.Base):
"""A list of CloudKittyResources."""
resources = [CloudkittyResource]
class CloudkittyModule(wtypes.Base):
"""A billing extension summary
"""
module_id = wtypes.wsattr(wtypes.text, mandatory=True)
"""Name of the extension."""
description = wtypes.wsattr(wtypes.text, mandatory=False)
"""Short description of the extension."""
enabled = wtypes.wsattr(bool, default=False)
"""Extension status."""
hot_config = wtypes.wsattr(bool, default=False, name='hot-config')
"""On-the-fly configuration support."""
@classmethod
def sample(cls):
sample = cls(name='example',
description='Sample extension.',
enabled=True,
hot_config=False)
return sample
class CloudkittyModuleCollection(wtypes.Base):
"""A list of billing extensions."""
modules = [CloudkittyModule]
deprecated()

View File

@ -0,0 +1,100 @@
# -*- coding: utf-8 -*-
# Copyright 2014 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 oslo.config import cfg
from wsme import types as wtypes
from cloudkitty.api.v1 import types as cktypes
CONF = cfg.CONF
CONF.import_opt('services', 'cloudkitty.collector', 'collect')
CLOUDKITTY_SERVICES = wtypes.Enum(wtypes.text,
*CONF.collect.services)
class CloudkittyResource(wtypes.Base):
"""Type describing a resource in CloudKitty.
"""
service = CLOUDKITTY_SERVICES
"""Name of the service."""
# FIXME(sheeprine): values should be dynamic
# Testing with ironic dynamic type
desc = {wtypes.text: cktypes.MultiType(wtypes.text, int, float, dict)}
"""Description of the resources parameters."""
volume = decimal.Decimal
"""Volume of resources."""
def to_json(self):
res_dict = {}
res_dict[self.service] = [{'desc': self.desc,
'vol': {'qty': self.volume,
'unit': 'undef'}
}]
return res_dict
@classmethod
def sample(cls):
sample = cls(service='compute',
desc={
'image_id': 'a41fba37-2429-4f15-aa00-b5bc4bf557bf'
},
volume=decimal.Decimal(1))
return sample
class CloudkittyResourceCollection(wtypes.Base):
"""A list of CloudKittyResources."""
resources = [CloudkittyResource]
class CloudkittyModule(wtypes.Base):
"""A rating extension summary
"""
module_id = wtypes.wsattr(wtypes.text, mandatory=True)
"""Name of the extension."""
description = wtypes.wsattr(wtypes.text, mandatory=False)
"""Short description of the extension."""
enabled = wtypes.wsattr(bool, default=False)
"""Extension status."""
hot_config = wtypes.wsattr(bool, default=False, name='hot-config')
"""On-the-fly configuration support."""
@classmethod
def sample(cls):
sample = cls(name='example',
description='Sample extension.',
enabled=True,
hot_config=False)
return sample
class CloudkittyModuleCollection(wtypes.Base):
"""A list of rating extensions."""
modules = [CloudkittyModule]

View File

@ -20,17 +20,17 @@ import decimal
from wsme import types as wtypes
from cloudkitty.api.v1.datamodels import billing as billing_resources
from cloudkitty.api.v1.datamodels import rating as rating_resources
class RatedResource(billing_resources.CloudkittyResource):
class RatedResource(rating_resources.CloudkittyResource):
"""Represents a rated CloudKitty resource."""
billing = decimal.Decimal
rating = decimal.Decimal
def to_json(self):
res_dict = super(RatedResource, self).to_json()
res_dict['billing'] = self.billing
res_dict['rating'] = self.rating
return res_dict

View File

@ -15,97 +15,24 @@
#
# @author: Stéphane Albert
#
import abc
import warnings
import six
from cloudkitty.db import api as db_api
from cloudkitty import rpc
from cloudkitty import rating
@six.add_metaclass(abc.ABCMeta)
class BillingProcessorBase(object):
def deprecated():
warnings.warn(
('The billing processors are deprecated. '
'Please use rating\'s one instead.'),
DeprecationWarning,
stacklevel=3)
deprecated()
class BillingProcessorBase(rating.RatingProcessorBase):
"""Provides the Cloudkitty integration code to the billing processors.
Every billing processor shoud sublclass this and override at least
module_name, description.
config_controller can be left at None to use the default one.
Deprecated, please use RatingProcessorBase.
"""
module_name = None
description = None
config_controller = None
hot_config = False
@property
def module_info(self):
return {
'name': self.module_name,
'description': self.description,
'hot_config': self.hot_config,
'enabled': self.enabled, }
def __init__(self, tenant_id=None):
self._tenant_id = tenant_id
@abc.abstractproperty
def enabled(self):
"""Check if the module is enabled
:returns: bool if module is enabled
"""
def set_state(self, enabled):
"""Enable or disable a module
:param enabled: (bool) The state to put the module in.
:return: bool
"""
api = db_api.get_instance()
module_db = api.get_module_enable_state()
client = rpc.get_client().prepare(namespace='billing',
fanout=True)
if enabled:
operation = 'enable_module'
else:
operation = 'disable_module'
client.cast({}, operation, name=self.module_name)
return module_db.set_state(self.module_name, enabled)
def quote(self, data):
"""Compute rating informations from data.
:param data: An internal CloudKitty dictionary used to describe
resources.
:type data: dict(str:?)
"""
return self.process(data)
def nodata(self, begin, end):
"""Handle billing processing when no data has been collected.
:param begin: Begin of the period.
:param end: End of the period.
"""
pass
@abc.abstractmethod
def process(self, data):
"""Add billing informations to data
:param data: An internal CloudKitty dictionary used to describe
resources.
:type data: dict(str:?)
"""
@abc.abstractmethod
def reload_config(self):
"""Trigger configuration reload
"""
def notify_reload(self):
client = rpc.get_rpc_client().prepare(namespace='billing',
fanout=True)
client.cast({}, 'reload_module', name=self.module_name)

View File

@ -15,163 +15,17 @@
#
# @author: Stéphane Albert
#
from cloudkitty import billing
from cloudkitty.billing.hash.controllers import root as root_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
import warnings
LOG = logging.getLogger(__name__)
from cloudkitty.rating.hash import * # noqa
class HashMap(billing.BillingProcessorBase):
"""HashMap rating module.
def deprecated():
warnings.warn(
('The hashmap billing processors are deprecated. '
'Please use rating\'s one instead.'),
DeprecationWarning,
stacklevel=3)
HashMap can be used to map arbitrary fields of a resource to different
costs.
"""
module_name = 'hashmap'
description = 'Basic hashmap billing module.'
hot_config = True
config_controller = root_api.HashMapConfigController
db_api = hash_db_api.get_instance()
def __init__(self, tenant_id=None):
super(HashMap, self).__init__(tenant_id)
self._service_mappings = {}
self._field_mappings = {}
self._res = {}
self._load_billing_rates()
@property
def enabled(self):
"""Check if the module is enabled
:returns: bool if module is enabled
"""
db_api = ck_db_api.get_instance()
module_db = db_api.get_module_enable_state()
return module_db.get_state('hashmap') or False
def reload_config(self):
"""Reload the module's configuration.
"""
self._load_billing_rates()
def _load_mappings(self, mappings_uuid_list):
hashmap = hash_db_api.get_instance()
mappings = {}
for mapping_uuid in mappings_uuid_list:
mapping_db = hashmap.get_mapping(uuid=mapping_uuid)
if mapping_db.group_id:
group_name = mapping_db.group.name
else:
group_name = '_DEFAULT_'
if group_name not in mappings:
mappings[group_name] = {}
mapping_value = mapping_db.value
map_dict = {}
map_dict['cost'] = mapping_db.cost
map_dict['type'] = mapping_db.map_type
if mapping_value:
mappings[group_name][mapping_value] = map_dict
else:
mappings[group_name] = map_dict
return mappings
def _load_service_mappings(self, service_name, service_uuid):
hashmap = hash_db_api.get_instance()
mappings_uuid_list = hashmap.list_mappings(service_uuid=service_uuid)
mappings = self._load_mappings(mappings_uuid_list)
if mappings:
self._service_mappings[service_name] = mappings
def _load_field_mappings(self, service_name, field_name, field_uuid):
hashmap = hash_db_api.get_instance()
mappings_uuid_list = hashmap.list_mappings(field_uuid=field_uuid)
mappings = self._load_mappings(mappings_uuid_list)
if mappings:
self._field_mappings[service_name] = {}
self._field_mappings[service_name][field_name] = mappings
def _load_billing_rates(self):
self._service_mappings = {}
self._field_mappings = {}
hashmap = hash_db_api.get_instance()
services_uuid_list = hashmap.list_services()
for service_uuid in services_uuid_list:
service_db = hashmap.get_service(uuid=service_uuid)
service_name = service_db.name
self._load_service_mappings(service_name, service_uuid)
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
cur_flat = self._res[group]['flat']
if new_flat > cur_flat:
self._res[group]['flat'] = new_flat
def process_service_map(self, service_name, data):
if service_name not in self._service_mappings:
return
serv_map = self._service_mappings[service_name]
for group_name, mapping in serv_map.items():
self.update_result(group_name,
mapping['type'],
mapping['cost'])
def process_field_map(self, service_name, data):
if service_name not in self._field_mappings:
return {}
field_map = self._field_mappings[service_name]
desc_data = data['desc']
for field_name, group_mappings in field_map.items():
if field_name not in desc_data:
continue
for group_name, mappings in group_mappings.items():
mapping_default = mappings.pop('_DEFAULT_', {})
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):
for cur_data in data:
cur_usage = cur_data['usage']
for service_name, service_data in cur_usage.items():
for item in service_data:
self._res = {}
self.process_service_map(service_name, item)
self.process_field_map(service_name, item)
self.add_billing_informations(item)
return data
deprecated()

View File

@ -0,0 +1,31 @@
# -*- 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 warnings
from cloudkitty.rating.hash.controllers import * # noqa
def deprecated():
warnings.warn(
('The hashmap billing controllers are deprecated. '
'Please use rating\'s one instead.'),
DeprecationWarning,
stacklevel=3)
deprecated()

View File

@ -15,86 +15,4 @@
#
# @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))
from cloudkitty.rating.hash.controllers.field import * # noqa

View File

@ -15,103 +15,4 @@
#
# @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))
from cloudkitty.rating.hash.controllers.group import * # noqa

View File

@ -15,149 +15,4 @@
#
# @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))
from cloudkitty.rating.hash.controllers.mapping import * # noqa

View File

@ -15,34 +15,4 @@
#
# @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
from cloudkitty.rating.hash.controllers.root import * # noqa

View File

@ -15,80 +15,4 @@
#
# @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))
from cloudkitty.rating.hash.controllers.service import * # noqa

View File

@ -0,0 +1,31 @@
# -*- 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 warnings
from cloudkitty.rating.hash.datamodels import * # noqa
def deprecated():
warnings.warn(
('The hashmap billing datamodels are deprecated. '
'Please use rating\'s one instead.'),
DeprecationWarning,
stacklevel=3)
deprecated()

View File

@ -15,47 +15,4 @@
#
# @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])
from cloudkitty.rating.hash.datamodels.field import * # noqa

View File

@ -15,44 +15,4 @@
#
# @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])
from cloudkitty.rating.hash.datamodels.group import * # noqa

View File

@ -15,68 +15,4 @@
#
# @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])
from cloudkitty.rating.hash.datamodels.mapping import * # noqa

View File

@ -15,41 +15,4 @@
#
# @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])
from cloudkitty.rating.hash.datamodels.service import * # noqa

View File

@ -0,0 +1,31 @@
# -*- coding: utf-8 -*-
# Copyright 2014 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 warnings
from cloudkitty.rating.hash.db import * # noqa
def deprecated():
warnings.warn(
('The hashmap db API is deprecated. '
'Please use rating\'s one instead.'),
DeprecationWarning,
stacklevel=3)
deprecated()

View File

@ -15,272 +15,4 @@
#
# @author: Stéphane Albert
#
import abc
from oslo.config import cfg
from oslo.db import api as db_api
import six
_BACKEND_MAPPING = {'sqlalchemy': 'cloudkitty.billing.hash.db.sqlalchemy.api'}
IMPL = db_api.DBAPI.from_config(cfg.CONF,
backend_mapping=_BACKEND_MAPPING,
lazy=True)
def get_instance():
"""Return a DB API instance."""
return IMPL
class NoSuchService(Exception):
"""Raised when the service doesn't exist."""
def __init__(self, name=None, uuid=None):
super(NoSuchService, self).__init__(
"No such service: %s (UUID: %s)" % (name, uuid))
self.name = name
self.uuid = uuid
class NoSuchField(Exception):
"""Raised when the field doesn't exist for the service."""
def __init__(self, uuid):
super(NoSuchField, self).__init__(
"No such field: %s" % uuid)
self.uuid = uuid
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):
"""Raised when the mapping doesn't exist."""
def __init__(self, uuid):
msg = ("No such mapping: %s" % uuid)
super(NoSuchMapping, self).__init__(msg)
self.uuid = uuid
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):
"""Raised when the service already exists."""
def __init__(self, name, uuid):
super(ServiceAlreadyExists, self).__init__(
"Service %s already exists (UUID: %s)" % (name, uuid))
self.name = name
self.uuid = uuid
class FieldAlreadyExists(Exception):
"""Raised when the field already exists."""
def __init__(self, field, uuid):
super(FieldAlreadyExists, self).__init__(
"Field %s already exists (UUID: %s)" % (field, uuid))
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):
"""Raised when the mapping already exists."""
def __init__(self, mapping, uuid):
super(MappingAlreadyExists, self).__init__(
"Mapping %s already exists (UUID: %s)" % (mapping, uuid))
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)
class HashMap(object):
"""Base class for hashmap configuration."""
@abc.abstractmethod
def get_migration(self):
"""Return a migrate manager.
"""
@abc.abstractmethod
def get_service(self, name=None, uuid=None):
"""Return a service object.
:param name: Filter on a service name.
:param uuid: The uuid of the service to get.
"""
@abc.abstractmethod
def get_field(self, uuid=None, service_uuid=None, name=None):
"""Return a field object.
:param uuid: UUID of the field to get.
: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
def get_group(self, uuid):
"""Return a group object.
:param uuid: UUID of the group to get.
"""
@abc.abstractmethod
def get_mapping(self, uuid):
"""Return a mapping object.
:param uuid: UUID of the mapping to get.
"""
@abc.abstractmethod
def list_services(self):
"""Return an UUID list of every service.
"""
@abc.abstractmethod
def list_fields(self, service_uuid):
"""Return an UUID list of every field in a service.
:param service_uuid: The service UUID to filter on.
"""
@abc.abstractmethod
def list_groups(self):
"""Return an UUID list of every group.
"""
@abc.abstractmethod
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.
:param name: Name of the service to create.
"""
@abc.abstractmethod
def create_field(self, service_uuid, name):
"""Create a new field.
:param service_uuid: UUID of the parent service.
:param name: Name of the field.
"""
@abc.abstractmethod
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.
:param cost: Rating value to apply to this mapping.
:param map_type: The type of rating rule.
:param value: Value of the field this mapping is applying to.
:param service_id: Service the mapping is applying to.
:param field_id: Field the mapping is applying to.
:param group_id: The group of calculations to apply.
"""
@abc.abstractmethod
def update_mapping(self, uuid, **kwargs):
"""Update a mapping.
:param uuid UUID of the mapping to modify.
:param cost: Rating value to apply to this mapping.
:param map_type: The type of rating rule.
:param value: Value of the field this mapping is applying to.
:param group_id: The group of calculations to apply.
"""
@abc.abstractmethod
def delete_service(self, name=None, uuid=None):
"""Delete a service recursively.
:param name: Name of the service to delete.
:param uuid: UUID of the service to delete.
"""
@abc.abstractmethod
def delete_field(self, uuid):
"""Delete a field recursively.
:param uuid UUID of the 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
def delete_mapping(self, uuid):
"""Delete a mapping
:param uuid: UUID of the mapping to delete.
"""
from cloudkitty.rating.hash.db.api import * # noqa

View File

@ -0,0 +1,31 @@
# -*- coding: utf-8 -*-
# Copyright 2014 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 warnings
from cloudkitty.rating.hash.db.sqlalchemy import * # noqa
def deprecated():
warnings.warn(
('The hashmap db API is deprecated. '
'Please use rating\'s one instead.'),
DeprecationWarning,
stacklevel=3)
deprecated()

View File

@ -15,333 +15,4 @@
#
# @author: Stéphane Albert
#
from oslo.db import exception
from oslo.db.sqlalchemy import utils
from oslo.utils import uuidutils
import six
import sqlalchemy
from cloudkitty.billing.hash.db import api
from cloudkitty.billing.hash.db.sqlalchemy import migration
from cloudkitty.billing.hash.db.sqlalchemy import models
from cloudkitty import db
from cloudkitty.openstack.common import log as logging
LOG = logging.getLogger(__name__)
def get_backend():
return HashMap()
class HashMap(api.HashMap):
def get_migration(self):
return migration
def get_service(self, name=None, uuid=None):
session = db.get_session()
try:
q = session.query(models.HashMapService)
if name:
q = q.filter(
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
except sqlalchemy.orm.exc.NoResultFound:
raise api.NoSuchService(name=name, uuid=uuid)
def get_field(self, uuid=None, service_uuid=None, name=None):
session = db.get_session()
try:
q = session.query(models.HashMapField)
if uuid:
q = q.filter(
models.HashMapField.field_id == uuid)
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
except sqlalchemy.orm.exc.NoResultFound:
raise api.NoSuchField(uuid)
def get_group(self, uuid):
session = db.get_session()
try:
q = session.query(models.HashMapGroup)
q = q.filter(
models.HashMapGroup.group_id == uuid)
res = q.one()
return res
except sqlalchemy.orm.exc.NoResultFound:
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):
session = db.get_session()
q = session.query(models.HashMapService)
res = q.values(
models.HashMapService.service_id)
return [uuid[0] for uuid in res]
def list_fields(self, service_uuid):
session = db.get_session()
q = session.query(models.HashMapField)
q = q.join(
models.HashMapField.service)
q = q.filter(
models.HashMapService.service_id == service_uuid)
res = q.values(models.HashMapField.field_id)
return [uuid[0] for uuid in res]
def list_groups(self):
session = db.get_session()
q = session.query(models.HashMapGroup)
res = q.values(
models.HashMapGroup.group_id)
return [uuid[0] for uuid in res]
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()
try:
with session.begin():
service_db = models.HashMapService(name=name)
service_db.service_id = uuidutils.generate_uuid()
session.add(service_db)
return service_db
except exception.DBDuplicateEntry:
service_db = self.get_service(name=name)
raise api.ServiceAlreadyExists(
service_db.name,
service_db.service_id)
def create_field(self, service_uuid, name):
service_db = self.get_service(uuid=service_uuid)
session = db.get_session()
try:
with session.begin():
field_db = models.HashMapField(
service_id=service_db.id,
name=name,
field_id=uuidutils.generate_uuid())
session.add(field_db)
return field_db
except exception.DBDuplicateEntry:
field_db = self.get_field(service_uuid=service_uuid,
name=name)
raise api.FieldAlreadyExists(field_db.name, field_db.field_id)
def create_group(self, name):
session = db.get_session()
try:
with session.begin():
group_db = models.HashMapGroup(
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()
try:
with session.begin():
field_map = models.HashMapMapping(
mapping_id=uuidutils.generate_uuid(),
value=value,
cost=cost,
field_id=field_fk,
service_id=service_fk,
map_type=map_type)
if group_id:
field_map.group_id = group_db.id
session.add(field_map)
return field_map
except exception.DBDuplicateEntry:
raise api.MappingAlreadyExists(value, field_map.field_id)
except exception.DBError:
raise api.NoSuchType(map_type)
def update_mapping(self, uuid, **kwargs):
session = db.get_session()
try:
with session.begin():
q = session.query(models.HashMapMapping)
q = q.filter(
models.HashMapMapping.mapping_id == uuid
)
mapping_db = q.with_lockmode('update').one()
if kwargs:
# Resolve FK
if 'group_id' in kwargs:
group_id = kwargs.pop('group_id')
if group_id:
group_db = self.get_group(group_id)
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):
if hasattr(mapping_db, attribute):
setattr(mapping_db, attribute, value)
else:
raise ValueError('No such attribute: {}'.format(
attribute))
else:
raise ValueError('No attribute to update.')
return mapping_db
except sqlalchemy.orm.exc.NoResultFound:
raise api.NoSuchMapping(uuid)
def delete_service(self, name=None, uuid=None):
session = db.get_session()
q = utils.model_query(
models.HashMapService,
session
)
if name:
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:
raise api.NoSuchService(name, uuid)
def delete_field(self, uuid):
session = db.get_session()
q = utils.model_query(
models.HashMapField,
session
)
q = q.filter_by(
field_id=uuid
)
r = q.delete()
if not r:
raise api.NoSuchField(uuid)
def delete_group(self, uuid, recurse=True):
session = db.get_session()
q = utils.model_query(
models.HashMapGroup,
session
).filter_by(
group_id=uuid,
)
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:
raise api.NoSuchMapping(uuid)
from cloudkitty.rating.hash.db.api.sqlalchemy.api import * # noqa

View File

@ -15,33 +15,4 @@
#
# @author: Stéphane Albert
#
import os
from cloudkitty.common.db.alembic import migration
ALEMBIC_REPO = os.path.join(os.path.dirname(__file__), 'alembic')
def upgrade(revision):
config = migration.load_alembic_config(ALEMBIC_REPO)
return migration.upgrade(config, revision)
def downgrade(revision):
config = migration.load_alembic_config(ALEMBIC_REPO)
return migration.downgrade(config, revision)
def version():
config = migration.load_alembic_config(ALEMBIC_REPO)
return migration.version(config)
def revision(message, autogenerate):
config = migration.load_alembic_config(ALEMBIC_REPO)
return migration.revision(config, message, autogenerate)
def stamp(revision):
config = migration.load_alembic_config(ALEMBIC_REPO)
return migration.stamp(config, revision)
from cloudkitty.rating.hash.db.api.sqlalchemy.migration import * # noqa

View File

@ -15,199 +15,4 @@
#
# @author: Stéphane Albert
#
from oslo.db.sqlalchemy import models
import sqlalchemy
from sqlalchemy.ext import declarative
from sqlalchemy import orm
from sqlalchemy import schema
Base = declarative.declarative_base()
class HashMapBase(models.ModelBase):
__table_args__ = {'mysql_charset': "utf8",
'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):
"""An hashmap service.
"""
__tablename__ = 'hashmap_services'
id = sqlalchemy.Column(sqlalchemy.Integer,
primary_key=True)
service_id = sqlalchemy.Column(sqlalchemy.String(36),
nullable=False,
unique=True)
name = sqlalchemy.Column(
sqlalchemy.String(255),
nullable=False,
unique=True
)
fields = orm.relationship('HashMapField',
backref=orm.backref(
'service',
lazy='immediate'))
mappings = orm.relationship('HashMapMapping',
backref=orm.backref(
'service',
lazy='immediate'))
def __repr__(self):
return ('<HashMapService[{uuid}]: '
'service={service}>').format(
uuid=self.service_id,
service=self.name)
class HashMapField(Base, HashMapBase):
"""An hashmap field.
"""
__tablename__ = 'hashmap_fields'
fk_to_resolve = {'service_id': 'service.service_id'}
@declarative.declared_attr
def __table_args__(cls):
args = (schema.UniqueConstraint('field_id', 'name',
name='uniq_field'),
schema.UniqueConstraint('service_id', 'name',
name='uniq_map_service_field'),
HashMapBase.__table_args__,)
return args
id = sqlalchemy.Column(sqlalchemy.Integer,
primary_key=True)
field_id = sqlalchemy.Column(sqlalchemy.String(36),
nullable=False,
unique=True)
name = sqlalchemy.Column(sqlalchemy.String(255),
nullable=False)
service_id = sqlalchemy.Column(
sqlalchemy.Integer,
sqlalchemy.ForeignKey('hashmap_services.id',
ondelete='CASCADE'),
nullable=False
)
mappings = orm.relationship('HashMapMapping',
backref=orm.backref(
'field',
lazy='immediate'))
def __repr__(self):
return ('<HashMapField[{uuid}]: '
'field={field}>').format(
uuid=self.field_id,
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):
"""A mapping between a field a value and a type.
"""
__tablename__ = 'hashmap_maps'
fk_to_resolve = {'service_id': 'service.service_id',
'field_id': 'field.field_id',
'group_id': 'group.group_id'}
@declarative.declared_attr
def __table_args__(cls):
args = (schema.UniqueConstraint('value', 'field_id',
name='uniq_field_mapping'),
schema.UniqueConstraint('value', 'service_id',
name='uniq_service_mapping'),
HashMapBase.__table_args__,)
return args
id = sqlalchemy.Column(sqlalchemy.Integer,
primary_key=True)
mapping_id = sqlalchemy.Column(sqlalchemy.String(36),
nullable=False,
unique=True)
value = sqlalchemy.Column(sqlalchemy.String(255),
nullable=True)
cost = sqlalchemy.Column(sqlalchemy.Numeric(20, 8),
nullable=False)
map_type = sqlalchemy.Column(sqlalchemy.Enum('flat',
'rate',
name='enum_map_type'),
nullable=False)
service_id = sqlalchemy.Column(sqlalchemy.Integer,
sqlalchemy.ForeignKey('hashmap_services.id',
ondelete='CASCADE'),
nullable=True)
field_id = sqlalchemy.Column(sqlalchemy.Integer,
sqlalchemy.ForeignKey('hashmap_fields.id',
ondelete='CASCADE'),
nullable=True)
group_id = sqlalchemy.Column(sqlalchemy.Integer,
sqlalchemy.ForeignKey('hashmap_groups.id',
ondelete='SET NULL'),
nullable=True)
def __repr__(self):
return ('<HashMapMapping[{uuid}]: '
'type={map_type} {value}={cost}>').format(
uuid=self.mapping_id,
map_type=self.map_type,
value=self.value,
cost=self.cost)
from cloudkitty.rating.hash.db.api.sqlalchemy.models import * # noqa

View File

@ -15,30 +15,4 @@
#
# @author: Stéphane Albert
#
from cloudkitty import billing
class Noop(billing.BillingProcessorBase):
module_name = "noop"
description = 'Dummy test module.'
@property
def enabled(self):
"""Check if the module is enabled
:returns: bool if module is enabled
"""
return True
def reload_config(self):
pass
def process(self, data):
for cur_data in data:
cur_usage = cur_data['usage']
for service in cur_usage:
for entry in cur_usage[service]:
if 'billing' not in entry:
entry['billing'] = {'price': 0}
return data
from cloudkitty.rating.noop import * # noqa

View File

@ -23,6 +23,7 @@ from cloudkitty.db import api as db_api
from cloudkitty import service
CONF = cfg.CONF
PROCESSORS_NAMESPACE = 'cloudkitty.rating.processors'
class ModuleNotFound(Exception):
@ -43,23 +44,23 @@ class MultipleModulesRevisions(Exception):
class DBCommand(object):
def __init__(self):
self.billing_models = {}
self._load_billing_models()
self.rating_models = {}
self._load_rating_models()
def _load_billing_models(self):
def _load_rating_models(self):
extensions = extension.ExtensionManager(
'cloudkitty.billing.processors')
self.billing_models = {}
PROCESSORS_NAMESPACE)
self.rating_models = {}
for ext in extensions:
if hasattr(ext.plugin, 'db_api'):
self.billing_models[ext.name] = ext.plugin.db_api
self.rating_models[ext.name] = ext.plugin.db_api
def get_module_migration(self, name):
if name == 'cloudkitty':
mod_migration = db_api.get_instance().get_migration()
else:
try:
module = self.billing_models[name]
module = self.rating_models[name]
mod_migration = module.get_migration()
except IndexError:
raise ModuleNotFound(name)
@ -69,7 +70,7 @@ class DBCommand(object):
if not name:
migrations = []
migrations.append(self.get_module_migration('cloudkitty'))
for model in self.billing_models.values():
for model in self.rating_models.values():
migrations.append(model.get_migration())
else:
return [self.get_module_migration(name)]

View File

@ -31,7 +31,7 @@ collect_opts = [
help='Number of samples to collect per call.'),
cfg.IntOpt('period',
default=3600,
help='Billing period in seconds.'),
help='Rating period in seconds.'),
cfg.IntOpt('wait_periods',
default=2,
help='Wait for N periods before collecting new data.'),

View File

@ -19,7 +19,7 @@ from stevedore import enabled
class EnabledExtensionManager(enabled.EnabledExtensionManager):
"""CloudKitty Billing processor manager
"""CloudKitty Rating processor manager
Override default EnabledExtensionManager to check for an internal
object property in the extension.

View File

@ -45,12 +45,12 @@ CONF.import_opt('backend', 'cloudkitty.storage', 'storage')
COLLECTORS_NAMESPACE = 'cloudkitty.collector.backends'
TRANSFORMERS_NAMESPACE = 'cloudkitty.transformers'
PROCESSORS_NAMESPACE = 'cloudkitty.billing.processors'
PROCESSORS_NAMESPACE = 'cloudkitty.rating.processors'
STORAGES_NAMESPACE = 'cloudkitty.storage.backends'
class BillingEndpoint(object):
target = messaging.Target(namespace='billing',
class RatingEndpoint(object):
target = messaging.Target(namespace='rating',
version='1.0')
def __init__(self, orchestrator):
@ -103,11 +103,11 @@ class BaseWorker(object):
def __init__(self, tenant_id=None):
self._tenant_id = tenant_id
# Billing processors
# Rating processors
self._processors = {}
self._load_billing_processors()
self._load_rating_processors()
def _load_billing_processors(self):
def _load_rating_processors(self):
self._processors = {}
processors = extension_manager.EnabledExtensionManager(
PROCESSORS_NAMESPACE,
@ -132,7 +132,7 @@ class APIWorker(BaseWorker):
for res in res_data:
for res_usage in res['usage'].values():
for data in res_usage:
price += data.get('billing', {}).get('price', 0.0)
price += data.get('rating', {}).get('price', 0.0)
return price
@ -179,7 +179,7 @@ class Worker(BaseWorker):
for service in CONF.collect.services:
try:
data = self._collect(service, timestamp)
# Billing
# Rating
for processor in self._processors.values():
processor.process(data)
# Writing
@ -232,7 +232,7 @@ class Orchestrator(object):
# RPC
self.server = None
self._billing_endpoint = BillingEndpoint(self)
self._rating_endpoint = RatingEndpoint(self)
self._init_messaging()
def _load_tenant_list(self):
@ -254,7 +254,7 @@ class Orchestrator(object):
server=CONF.host,
version='1.0')
endpoints = [
self._billing_endpoint,
self._rating_endpoint,
]
self.server = rpc.get_server(target, endpoints)
self.server.start()
@ -297,8 +297,8 @@ class Orchestrator(object):
def process_messages(self):
# TODO(sheeprine): Code kept to handle threading and asynchronous
# reloading
# pending_reload = self._billing_endpoint.get_reload_list()
# pending_states = self._billing_endpoint.get_module_state()
# pending_reload = self._rating_endpoint.get_reload_list()
# pending_states = self._rating_endpoint.get_module_state()
pass
def process(self):

View File

@ -0,0 +1,103 @@
# -*- coding: utf-8 -*-
# Copyright 2014 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 abc
import six
from cloudkitty.db import api as db_api
from cloudkitty import rpc
@six.add_metaclass(abc.ABCMeta)
class RatingProcessorBase(object):
"""Provides the Cloudkitty integration code to the rating processors.
Every rating processor shoud sublclass this and override at least
module_name, description.
config_controller can be left at None to use the default one.
"""
module_name = None
description = None
config_controller = None
hot_config = False
@property
def module_info(self):
return {
'name': self.module_name,
'description': self.description,
'hot_config': self.hot_config,
'enabled': self.enabled, }
def __init__(self, tenant_id=None):
self._tenant_id = tenant_id
@abc.abstractproperty
def enabled(self):
"""Check if the module is enabled
:returns: bool if module is enabled
"""
def set_state(self, enabled):
"""Enable or disable a module
:param enabled: (bool) The state to put the module in.
:return: bool
"""
api = db_api.get_instance()
module_db = api.get_module_enable_state()
client = rpc.get_client().prepare(namespace='rating',
fanout=True)
if enabled:
operation = 'enable_module'
else:
operation = 'disable_module'
client.cast({}, operation, name=self.module_name)
return module_db.set_state(self.module_name, enabled)
def quote(self, data):
"""Compute rating informations from data.
:param data: An internal CloudKitty dictionary used to describe
resources.
:type data: dict(str:?)
"""
return self.process(data)
@abc.abstractmethod
def process(self, data):
"""Add rating informations to data
:param data: An internal CloudKitty dictionary used to describe
resources.
:type data: dict(str:?)
"""
@abc.abstractmethod
def reload_config(self):
"""Trigger configuration reload
"""
def notify_reload(self):
client = rpc.get_rpc_client().prepare(namespace='rating',
fanout=True)
client.cast({}, 'reload_module', name=self.module_name)

View File

@ -0,0 +1,177 @@
# -*- coding: utf-8 -*-
# Copyright 2014 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 cloudkitty.db import api as ck_db_api
from cloudkitty.openstack.common import log as logging
from cloudkitty import rating
from cloudkitty.rating.hash.controllers import root as root_api
from cloudkitty.rating.hash.db import api as hash_db_api
LOG = logging.getLogger(__name__)
class HashMap(rating.RatingProcessorBase):
"""HashMap rating module.
HashMap can be used to map arbitrary fields of a resource to different
costs.
"""
module_name = 'hashmap'
description = 'HashMap rating module.'
hot_config = True
config_controller = root_api.HashMapConfigController
db_api = hash_db_api.get_instance()
def __init__(self, tenant_id=None):
super(HashMap, self).__init__(tenant_id)
self._service_mappings = {}
self._field_mappings = {}
self._res = {}
self._load_rates()
@property
def enabled(self):
"""Check if the module is enabled
:returns: bool if module is enabled
"""
db_api = ck_db_api.get_instance()
module_db = db_api.get_module_enable_state()
return module_db.get_state('hashmap') or False
def reload_config(self):
"""Reload the module's configuration.
"""
self._load_rates()
def _load_mappings(self, mappings_uuid_list):
hashmap = hash_db_api.get_instance()
mappings = {}
for mapping_uuid in mappings_uuid_list:
mapping_db = hashmap.get_mapping(uuid=mapping_uuid)
if mapping_db.group_id:
group_name = mapping_db.group.name
else:
group_name = '_DEFAULT_'
if group_name not in mappings:
mappings[group_name] = {}
mapping_value = mapping_db.value
map_dict = {}
map_dict['cost'] = mapping_db.cost
map_dict['type'] = mapping_db.map_type
if mapping_value:
mappings[group_name][mapping_value] = map_dict
else:
mappings[group_name] = map_dict
return mappings
def _load_service_mappings(self, service_name, service_uuid):
hashmap = hash_db_api.get_instance()
mappings_uuid_list = hashmap.list_mappings(service_uuid=service_uuid)
mappings = self._load_mappings(mappings_uuid_list)
if mappings:
self._service_mappings[service_name] = mappings
def _load_field_mappings(self, service_name, field_name, field_uuid):
hashmap = hash_db_api.get_instance()
mappings_uuid_list = hashmap.list_mappings(field_uuid=field_uuid)
mappings = self._load_mappings(mappings_uuid_list)
if mappings:
self._field_mappings[service_name] = {}
self._field_mappings[service_name][field_name] = mappings
def _load_rates(self):
self._service_mappings = {}
self._field_mappings = {}
hashmap = hash_db_api.get_instance()
services_uuid_list = hashmap.list_services()
for service_uuid in services_uuid_list:
service_db = hashmap.get_service(uuid=service_uuid)
service_name = service_db.name
self._load_service_mappings(service_name, service_uuid)
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_rating_informations(self, data):
if 'rating' not in data:
data['rating'] = {'price': 0}
for entry in self._res.values():
res = entry['rate'] * entry['flat']
data['rating']['price'] += res * data['vol']['qty']
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
cur_flat = self._res[group]['flat']
if new_flat > cur_flat:
self._res[group]['flat'] = new_flat
def process_service_map(self, service_name, data):
if service_name not in self._service_mappings:
return
serv_map = self._service_mappings[service_name]
for group_name, mapping in serv_map.items():
self.update_result(group_name,
mapping['type'],
mapping['cost'])
def process_field_map(self, service_name, data):
if service_name not in self._field_mappings:
return {}
field_map = self._field_mappings[service_name]
desc_data = data['desc']
for field_name, group_mappings in field_map.items():
if field_name not in desc_data:
continue
for group_name, mappings in group_mappings.items():
mapping_default = mappings.pop('_DEFAULT_', {})
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):
for cur_data in data:
cur_usage = cur_data['usage']
for service_name, service_data in cur_usage.items():
for item in service_data:
self._res = {}
self.process_service_map(service_name, item)
self.process_field_map(service_name, item)
self.add_rating_informations(item)
return data

View File

@ -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.rating.hash.datamodels import field as field_models
from cloudkitty.rating.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))

View File

@ -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.rating.hash.datamodels import group as group_models
from cloudkitty.rating.hash.datamodels import mapping as mapping_models
from cloudkitty.rating.hash.db import api as db_api
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))

View File

@ -0,0 +1,163 @@
# -*- coding: utf-8 -*-
# Copyright 2015 Objectif Libre
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
#
# @author: Stéphane Albert
#
import pecan
from pecan import rest
import wsmeext.pecan as wsme_pecan
from cloudkitty.api.v1 import types as ck_types
from cloudkitty.rating.hash.datamodels import group as group_models
from cloudkitty.rating.hash.datamodels import mapping as mapping_models
from cloudkitty.rating.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))

View File

@ -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.rating.hash.controllers import field as field_api
from cloudkitty.rating.hash.controllers import group as group_api
from cloudkitty.rating.hash.controllers import mapping as mapping_api
from cloudkitty.rating.hash.controllers import service as service_api
from cloudkitty.rating.hash.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

View File

@ -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.rating.hash.controllers import field as field_api
from cloudkitty.rating.hash.datamodels import service as service_models
from cloudkitty.rating.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))

View File

@ -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])

View File

@ -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])

View File

@ -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])

View File

@ -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])

View File

View File

@ -0,0 +1,286 @@
# -*- coding: utf-8 -*-
# Copyright 2014 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 abc
from oslo.config import cfg
from oslo.db import api as db_api
import six
_BACKEND_MAPPING = {'sqlalchemy': 'cloudkitty.rating.hash.db.sqlalchemy.api'}
IMPL = db_api.DBAPI.from_config(cfg.CONF,
backend_mapping=_BACKEND_MAPPING,
lazy=True)
def get_instance():
"""Return a DB API instance."""
return IMPL
class NoSuchService(Exception):
"""Raised when the service doesn't exist."""
def __init__(self, name=None, uuid=None):
super(NoSuchService, self).__init__(
"No such service: %s (UUID: %s)" % (name, uuid))
self.name = name
self.uuid = uuid
class NoSuchField(Exception):
"""Raised when the field doesn't exist for the service."""
def __init__(self, uuid):
super(NoSuchField, self).__init__(
"No such field: %s" % uuid)
self.uuid = uuid
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):
"""Raised when the mapping doesn't exist."""
def __init__(self, uuid):
msg = ("No such mapping: %s" % uuid)
super(NoSuchMapping, self).__init__(msg)
self.uuid = uuid
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):
"""Raised when the service already exists."""
def __init__(self, name, uuid):
super(ServiceAlreadyExists, self).__init__(
"Service %s already exists (UUID: %s)" % (name, uuid))
self.name = name
self.uuid = uuid
class FieldAlreadyExists(Exception):
"""Raised when the field already exists."""
def __init__(self, field, uuid):
super(FieldAlreadyExists, self).__init__(
"Field %s already exists (UUID: %s)" % (field, uuid))
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):
"""Raised when the mapping already exists."""
def __init__(self, mapping, uuid):
super(MappingAlreadyExists, self).__init__(
"Mapping %s already exists (UUID: %s)" % (mapping, uuid))
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)
class HashMap(object):
"""Base class for hashmap configuration."""
@abc.abstractmethod
def get_migration(self):
"""Return a migrate manager.
"""
@abc.abstractmethod
def get_service(self, name=None, uuid=None):
"""Return a service object.
:param name: Filter on a service name.
:param uuid: The uuid of the service to get.
"""
@abc.abstractmethod
def get_field(self, uuid=None, service_uuid=None, name=None):
"""Return a field object.
:param uuid: UUID of the field to get.
: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
def get_group(self, uuid):
"""Return a group object.
:param uuid: UUID of the group to get.
"""
@abc.abstractmethod
def get_mapping(self, uuid):
"""Return a mapping object.
:param uuid: UUID of the mapping to get.
"""
@abc.abstractmethod
def list_services(self):
"""Return an UUID list of every service.
"""
@abc.abstractmethod
def list_fields(self, service_uuid):
"""Return an UUID list of every field in a service.
:param service_uuid: The service UUID to filter on.
"""
@abc.abstractmethod
def list_groups(self):
"""Return an UUID list of every group.
"""
@abc.abstractmethod
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.
:param name: Name of the service to create.
"""
@abc.abstractmethod
def create_field(self, service_uuid, name):
"""Create a new field.
:param service_uuid: UUID of the parent service.
:param name: Name of the field.
"""
@abc.abstractmethod
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.
:param cost: Rating value to apply to this mapping.
:param map_type: The type of rating rule.
:param value: Value of the field this mapping is applying to.
:param service_id: Service the mapping is applying to.
:param field_id: Field the mapping is applying to.
:param group_id: The group of calculations to apply.
"""
@abc.abstractmethod
def update_mapping(self, uuid, **kwargs):
"""Update a mapping.
:param uuid UUID of the mapping to modify.
:param cost: Rating value to apply to this mapping.
:param map_type: The type of rating rule.
:param value: Value of the field this mapping is applying to.
:param group_id: The group of calculations to apply.
"""
@abc.abstractmethod
def delete_service(self, name=None, uuid=None):
"""Delete a service recursively.
:param name: Name of the service to delete.
:param uuid: UUID of the service to delete.
"""
@abc.abstractmethod
def delete_field(self, uuid):
"""Delete a field recursively.
:param uuid UUID of the 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
def delete_mapping(self, uuid):
"""Delete a mapping
:param uuid: UUID of the mapping to delete.
"""

View File

@ -15,8 +15,8 @@
#
# @author: Stéphane Albert
#
from cloudkitty.billing.hash.db.sqlalchemy import models
from cloudkitty.common.db.alembic import env # noqa
from cloudkitty.rating.hash.db.sqlalchemy import models
target_metadata = models.Base.metadata
version_table = 'hashmap_alembic'

View File

@ -0,0 +1,347 @@
# -*- coding: utf-8 -*-
# Copyright 2014 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 oslo.db import exception
from oslo.db.sqlalchemy import utils
from oslo.utils import uuidutils
import six
import sqlalchemy
from cloudkitty import db
from cloudkitty.openstack.common import log as logging
from cloudkitty.rating.hash.db import api
from cloudkitty.rating.hash.db.sqlalchemy import migration
from cloudkitty.rating.hash.db.sqlalchemy import models
LOG = logging.getLogger(__name__)
def get_backend():
return HashMap()
class HashMap(api.HashMap):
def get_migration(self):
return migration
def get_service(self, name=None, uuid=None):
session = db.get_session()
try:
q = session.query(models.HashMapService)
if name:
q = q.filter(
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
except sqlalchemy.orm.exc.NoResultFound:
raise api.NoSuchService(name=name, uuid=uuid)
def get_field(self, uuid=None, service_uuid=None, name=None):
session = db.get_session()
try:
q = session.query(models.HashMapField)
if uuid:
q = q.filter(
models.HashMapField.field_id == uuid)
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
except sqlalchemy.orm.exc.NoResultFound:
raise api.NoSuchField(uuid)
def get_group(self, uuid):
session = db.get_session()
try:
q = session.query(models.HashMapGroup)
q = q.filter(
models.HashMapGroup.group_id == uuid)
res = q.one()
return res
except sqlalchemy.orm.exc.NoResultFound:
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):
session = db.get_session()
q = session.query(models.HashMapService)
res = q.values(
models.HashMapService.service_id)
return [uuid[0] for uuid in res]
def list_fields(self, service_uuid):
session = db.get_session()
q = session.query(models.HashMapField)
q = q.join(
models.HashMapField.service)
q = q.filter(
models.HashMapService.service_id == service_uuid)
res = q.values(models.HashMapField.field_id)
return [uuid[0] for uuid in res]
def list_groups(self):
session = db.get_session()
q = session.query(models.HashMapGroup)
res = q.values(
models.HashMapGroup.group_id)
return [uuid[0] for uuid in res]
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()
try:
with session.begin():
service_db = models.HashMapService(name=name)
service_db.service_id = uuidutils.generate_uuid()
session.add(service_db)
return service_db
except exception.DBDuplicateEntry:
service_db = self.get_service(name=name)
raise api.ServiceAlreadyExists(
service_db.name,
service_db.service_id)
def create_field(self, service_uuid, name):
service_db = self.get_service(uuid=service_uuid)
session = db.get_session()
try:
with session.begin():
field_db = models.HashMapField(
service_id=service_db.id,
name=name,
field_id=uuidutils.generate_uuid())
session.add(field_db)
return field_db
except exception.DBDuplicateEntry:
field_db = self.get_field(service_uuid=service_uuid,
name=name)
raise api.FieldAlreadyExists(field_db.name, field_db.field_id)
def create_group(self, name):
session = db.get_session()
try:
with session.begin():
group_db = models.HashMapGroup(
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()
try:
with session.begin():
field_map = models.HashMapMapping(
mapping_id=uuidutils.generate_uuid(),
value=value,
cost=cost,
field_id=field_fk,
service_id=service_fk,
map_type=map_type)
if group_id:
field_map.group_id = group_db.id
session.add(field_map)
return field_map
except exception.DBDuplicateEntry:
raise api.MappingAlreadyExists(value, field_map.field_id)
except exception.DBError:
raise api.NoSuchType(map_type)
def update_mapping(self, uuid, **kwargs):
session = db.get_session()
try:
with session.begin():
q = session.query(models.HashMapMapping)
q = q.filter(
models.HashMapMapping.mapping_id == uuid
)
mapping_db = q.with_lockmode('update').one()
if kwargs:
# Resolve FK
if 'group_id' in kwargs:
group_id = kwargs.pop('group_id')
if group_id:
group_db = self.get_group(group_id)
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):
if hasattr(mapping_db, attribute):
setattr(mapping_db, attribute, value)
else:
raise ValueError('No such attribute: {}'.format(
attribute))
else:
raise ValueError('No attribute to update.')
return mapping_db
except sqlalchemy.orm.exc.NoResultFound:
raise api.NoSuchMapping(uuid)
def delete_service(self, name=None, uuid=None):
session = db.get_session()
q = utils.model_query(
models.HashMapService,
session
)
if name:
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:
raise api.NoSuchService(name, uuid)
def delete_field(self, uuid):
session = db.get_session()
q = utils.model_query(
models.HashMapField,
session
)
q = q.filter_by(
field_id=uuid
)
r = q.delete()
if not r:
raise api.NoSuchField(uuid)
def delete_group(self, uuid, recurse=True):
session = db.get_session()
q = utils.model_query(
models.HashMapGroup,
session
).filter_by(
group_id=uuid,
)
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:
raise api.NoSuchMapping(uuid)

View File

@ -0,0 +1,47 @@
# -*- coding: utf-8 -*-
# Copyright 2014 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 os
from cloudkitty.common.db.alembic import migration
ALEMBIC_REPO = os.path.join(os.path.dirname(__file__), 'alembic')
def upgrade(revision):
config = migration.load_alembic_config(ALEMBIC_REPO)
return migration.upgrade(config, revision)
def downgrade(revision):
config = migration.load_alembic_config(ALEMBIC_REPO)
return migration.downgrade(config, revision)
def version():
config = migration.load_alembic_config(ALEMBIC_REPO)
return migration.version(config)
def revision(message, autogenerate):
config = migration.load_alembic_config(ALEMBIC_REPO)
return migration.revision(config, message, autogenerate)
def stamp(revision):
config = migration.load_alembic_config(ALEMBIC_REPO)
return migration.stamp(config, revision)

View File

@ -0,0 +1,213 @@
# -*- coding: utf-8 -*-
# Copyright 2014 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 oslo.db.sqlalchemy import models
import sqlalchemy
from sqlalchemy.ext import declarative
from sqlalchemy import orm
from sqlalchemy import schema
Base = declarative.declarative_base()
class HashMapBase(models.ModelBase):
__table_args__ = {'mysql_charset': "utf8",
'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):
"""An hashmap service.
"""
__tablename__ = 'hashmap_services'
id = sqlalchemy.Column(sqlalchemy.Integer,
primary_key=True)
service_id = sqlalchemy.Column(sqlalchemy.String(36),
nullable=False,
unique=True)
name = sqlalchemy.Column(
sqlalchemy.String(255),
nullable=False,
unique=True
)
fields = orm.relationship('HashMapField',
backref=orm.backref(
'service',
lazy='immediate'))
mappings = orm.relationship('HashMapMapping',
backref=orm.backref(
'service',
lazy='immediate'))
def __repr__(self):
return ('<HashMapService[{uuid}]: '
'service={service}>').format(
uuid=self.service_id,
service=self.name)
class HashMapField(Base, HashMapBase):
"""An hashmap field.
"""
__tablename__ = 'hashmap_fields'
fk_to_resolve = {'service_id': 'service.service_id'}
@declarative.declared_attr
def __table_args__(cls):
args = (schema.UniqueConstraint('field_id', 'name',
name='uniq_field'),
schema.UniqueConstraint('service_id', 'name',
name='uniq_map_service_field'),
HashMapBase.__table_args__,)
return args
id = sqlalchemy.Column(sqlalchemy.Integer,
primary_key=True)
field_id = sqlalchemy.Column(sqlalchemy.String(36),
nullable=False,
unique=True)
name = sqlalchemy.Column(sqlalchemy.String(255),
nullable=False)
service_id = sqlalchemy.Column(
sqlalchemy.Integer,
sqlalchemy.ForeignKey('hashmap_services.id',
ondelete='CASCADE'),
nullable=False
)
mappings = orm.relationship('HashMapMapping',
backref=orm.backref(
'field',
lazy='immediate'))
def __repr__(self):
return ('<HashMapField[{uuid}]: '
'field={field}>').format(
uuid=self.field_id,
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):
"""A mapping between a field a value and a type.
"""
__tablename__ = 'hashmap_maps'
fk_to_resolve = {'service_id': 'service.service_id',
'field_id': 'field.field_id',
'group_id': 'group.group_id'}
@declarative.declared_attr
def __table_args__(cls):
args = (schema.UniqueConstraint('value', 'field_id',
name='uniq_field_mapping'),
schema.UniqueConstraint('value', 'service_id',
name='uniq_service_mapping'),
HashMapBase.__table_args__,)
return args
id = sqlalchemy.Column(sqlalchemy.Integer,
primary_key=True)
mapping_id = sqlalchemy.Column(sqlalchemy.String(36),
nullable=False,
unique=True)
value = sqlalchemy.Column(sqlalchemy.String(255),
nullable=True)
cost = sqlalchemy.Column(sqlalchemy.Numeric(20, 8),
nullable=False)
map_type = sqlalchemy.Column(sqlalchemy.Enum('flat',
'rate',
name='enum_map_type'),
nullable=False)
service_id = sqlalchemy.Column(sqlalchemy.Integer,
sqlalchemy.ForeignKey('hashmap_services.id',
ondelete='CASCADE'),
nullable=True)
field_id = sqlalchemy.Column(sqlalchemy.Integer,
sqlalchemy.ForeignKey('hashmap_fields.id',
ondelete='CASCADE'),
nullable=True)
group_id = sqlalchemy.Column(sqlalchemy.Integer,
sqlalchemy.ForeignKey('hashmap_groups.id',
ondelete='SET NULL'),
nullable=True)
def __repr__(self):
return ('<HashMapMapping[{uuid}]: '
'type={map_type} {value}={cost}>').format(
uuid=self.mapping_id,
map_type=self.map_type,
value=self.value,
cost=self.cost)

44
cloudkitty/rating/noop.py Normal file
View File

@ -0,0 +1,44 @@
# -*- coding: utf-8 -*-
# Copyright 2014 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 cloudkitty import rating
class Noop(rating.RatingProcessorBase):
module_name = "noop"
description = 'Dummy test module.'
@property
def enabled(self):
"""Check if the module is enabled
:returns: bool if module is enabled
"""
return True
def reload_config(self):
pass
def process(self, data):
for cur_data in data:
cur_usage = cur_data['usage']
for service in cur_usage:
for entry in cur_usage[service]:
if 'rating' not in entry:
entry['rating'] = {'price': 0}
return data

View File

@ -42,7 +42,7 @@ class SQLAlchemyStorage(storage.BaseStorage):
def _pre_commit(self, tenant_id):
if not self._has_data:
empty_frame = {'vol': {'qty': 0, 'unit': 'None'},
'billing': {'price': 0}, 'desc': ''}
'rating': {'price': 0}, 'desc': ''}
self._append_time_frame('_NO_DATA_', empty_frame, tenant_id)
def _commit(self, tenant_id):
@ -143,7 +143,7 @@ class SQLAlchemyStorage(storage.BaseStorage):
vol_dict = frame['vol']
qty = vol_dict['qty']
unit = vol_dict['unit']
rating_dict = frame['billing']
rating_dict = frame['rating']
rate = rating_dict['price']
desc = json.dumps(frame['desc'])
self.add_time_frame(self.usage_start_dt.get(tenant_id),

View File

@ -65,7 +65,7 @@ class RatedDataFrame(Base, models.ModelBase):
res_dict = {}
# Encapsulate informations in a resource dict
res_dict['billing'] = rating_dict
res_dict['rating'] = rating_dict
res_dict['desc'] = json.loads(self.desc)
res_dict['vol'] = vol_dict
res_dict['tenant_id'] = self.tenant_id

View File

@ -20,8 +20,8 @@ import decimal
import mock
from oslo.utils import uuidutils
from cloudkitty.billing import hash
from cloudkitty.billing.hash.db import api
from cloudkitty.rating import hash
from cloudkitty.rating.hash.db import api
from cloudkitty import tests
TEST_TS = 1388577600
@ -391,7 +391,7 @@ class HashMapRatingTest(tests.TestCase):
self.assertEqual([], mappings)
# Processing tests
def test_load_billing_rates(self):
def test_load_rates(self):
service_db = self._db_api.create_service('compute')
field_db = self._db_api.create_field(service_db.service_id,
'flavor')
@ -450,9 +450,9 @@ class HashMapRatingTest(tests.TestCase):
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')}
compute_list[0]['rating'] = {'price': decimal.Decimal('2.757')}
compute_list[1]['rating'] = {'price': decimal.Decimal('2.757')}
compute_list[2]['rating'] = {'price': decimal.Decimal('2.757')}
self._hash.process(actual_data)
self.assertEqual(expected_data, actual_data)
@ -484,13 +484,13 @@ class HashMapRatingTest(tests.TestCase):
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')}
compute_list[0]['rating'] = {'price': decimal.Decimal('1.337')}
compute_list[1]['rating'] = {'price': decimal.Decimal('1.42')}
compute_list[2]['rating'] = {'price': decimal.Decimal('1.47070')}
self._hash.process(actual_data)
self.assertEqual(expected_data, actual_data)
def test_process_billing(self):
def test_process_rating(self):
service_db = self._db_api.create_service('compute')
field_db = self._db_api.create_field(service_db.service_id,
'flavor')
@ -513,8 +513,8 @@ class HashMapRatingTest(tests.TestCase):
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')}
compute_list[0]['rating'] = {'price': decimal.Decimal('1.337')}
compute_list[1]['rating'] = {'price': decimal.Decimal('1.42')}
compute_list[2]['rating'] = {'price': decimal.Decimal('1.42')}
self._hash.process(actual_data)
self.assertEqual(expected_data, actual_data)

View File

@ -87,7 +87,7 @@ class WriteOrchestrator(object):
for service in data:
# Update totals
for entry in data[service]:
self.total += entry['billing']['price']
self.total += entry['rating']['price']
# Dispatch data to writing pipeline
for backend in self._write_pipeline:
backend.append(data, self.usage_start, self.usage_end)

View File

@ -139,7 +139,7 @@ class BaseReportWriter(object):
self._usage_data[service] = data[service]
# Update totals
for entry in data[service]:
self.total += entry['billing']['price']
self.total += entry['rating']['price']
def append(self, data, start, end):
# FIXME we should use the real time values

View File

@ -45,7 +45,7 @@ The data format of CloudKitty is the following:
{
"myservice": [
{
"billing": {
"rating": {
"price": 0.1
},
"desc": {
@ -83,34 +83,17 @@ Rating
**Loaded with stevedore**
This is where every rating calculations is done. The data gathered by the
collector is pushed in a pipeline of billing processors. Every processor does
collector is pushed in a pipeline of rating processors. Every processor does
its calculations and updates the data.
Example of minimal rating module (taken from the Noop module):
.. code-block:: python
class NoopController(billing.BillingController):
module_name = 'noop'
def get_module_info(self):
module = Noop()
infos = {
'name': self.module_name,
'description': 'Dummy test module.',
'enabled': module.enabled,
'hot_config': False,
}
return infos
class Noop(billing.BillingProcessorBase):
class Noop(rating.RatingProcessorBase):
controller = NoopController
def __init__(self):
pass
description = 'Dummy test module'
@property
def enabled(self):
@ -128,8 +111,8 @@ Example of minimal rating module (taken from the Noop module):
cur_usage = cur_data['usage']
for service in cur_usage:
for entry in cur_usage[service]:
if 'billing' not in entry:
entry['billing'] = {'price': 0}
if 'rating' not in entry:
entry['rating'] = {'price': 0}
return data

View File

@ -48,7 +48,7 @@ Modules
:maxdepth: 1
:glob:
webapi/billing/*
webapi/rating/*
Indices and tables

View File

@ -1,42 +0,0 @@
=======================
HashMap Module REST API
=======================
.. rest-controller:: cloudkitty.billing.hash.controllers.root:HashMapConfigController
:webprefix: /v1/billing/module_config/hashmap
.. rest-controller:: cloudkitty.billing.hash.controllers.service:HashMapServicesController
:webprefix: /v1/billing/module_config/hashmap/services
.. autotype:: cloudkitty.billing.hash.datamodels.service.Service
: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:

View File

@ -0,0 +1,42 @@
=======================
HashMap Module REST API
=======================
.. rest-controller:: cloudkitty.rating.hash.controllers.root:HashMapConfigController
:webprefix: /v1/rating/module_config/hashmap
.. rest-controller:: cloudkitty.rating.hash.controllers.service:HashMapServicesController
:webprefix: /v1/rating/module_config/hashmap/services
.. autotype:: cloudkitty.rating.hash.datamodels.service.Service
:members:
.. autotype:: cloudkitty.rating.hash.datamodels.service.ServiceCollection
:members:
.. rest-controller:: cloudkitty.rating.hash.controllers.field:HashMapFieldsController
:webprefix: /v1/rating/module_config/hashmap/fields
.. autotype:: cloudkitty.rating.hash.datamodels.field.Field
:members:
.. autotype:: cloudkitty.rating.hash.datamodels.field.FieldCollection
:members:
.. rest-controller:: cloudkitty.rating.hash.controllers.mapping:HashMapMappingsController
:webprefix: /v1/rating/module_config/hashmap/mappings
.. autotype:: cloudkitty.rating.hash.datamodels.mapping.Mapping
:members:
.. autotype:: cloudkitty.rating.hash.datamodels.mapping.MappingCollection
:members:
.. rest-controller:: cloudkitty.rating.hash.controllers.group:HashMapGroupsController
:webprefix: /v1/rating/module_config/hashmap/groups
.. autotype:: cloudkitty.rating.hash.datamodels.group.Group
:members:
.. autotype:: cloudkitty.rating.hash.datamodels.group.GroupCollection
:members:

View File

@ -12,25 +12,25 @@ Collector
:webprefix: /v1/collector/mapping
Billing
=======
Rating
======
.. rest-controller:: cloudkitty.api.v1.controllers.billing:ModulesController
:webprefix: /v1/billing/modules
.. rest-controller:: cloudkitty.api.v1.controllers.rating:ModulesController
:webprefix: /v1/rating/modules
.. rest-controller:: cloudkitty.api.v1.controllers.billing:BillingController
:webprefix: /v1/billing
.. rest-controller:: cloudkitty.api.v1.controllers.rating:RatingController
:webprefix: /v1/rating
.. autotype:: cloudkitty.api.v1.datamodels.billing.CloudkittyModule
.. autotype:: cloudkitty.api.v1.datamodels.rating.CloudkittyModule
:members:
.. autotype:: cloudkitty.api.v1.datamodels.billing.CloudkittyModuleCollection
.. autotype:: cloudkitty.api.v1.datamodels.rating.CloudkittyModuleCollection
:members:
.. autotype:: cloudkitty.api.v1.datamodels.billing.CloudkittyResource
.. autotype:: cloudkitty.api.v1.datamodels.rating.CloudkittyResource
:members:
.. autotype:: cloudkitty.api.v1.datamodels.billing.CloudkittyResourceCollection
.. autotype:: cloudkitty.api.v1.datamodels.rating.CloudkittyResourceCollection
:members:

View File

@ -315,7 +315,7 @@
# Number of samples to collect per call. (integer value)
#window = 1800
# Billing period in seconds. (integer value)
# Rating period in seconds. (integer value)
#period = 3600
# Wait for N periods before collecting new data. (integer value)

View File

@ -1,6 +1,6 @@
[metadata]
name = cloudkitty
summary = OpenStack Billing and Usage Reporter
summary = OpenStack Rating and Usage Reporter
description-file =
README.rst
classifier =
@ -37,9 +37,9 @@ cloudkitty.transformers =
CloudKittyFormatTransformer = cloudkitty.transformer.format:CloudKittyFormatTransformer
CeilometerTransformer = cloudkitty.transformer.ceilometer:CeilometerTransformer
cloudkitty.billing.processors =
noop = cloudkitty.billing.noop:Noop
hashmap = cloudkitty.billing.hash:HashMap
cloudkitty.rating.processors =
noop = cloudkitty.rating.noop:Noop
hashmap = cloudkitty.rating.hash:HashMap
cloudkitty.storage.backends =
sqlalchemy = cloudkitty.storage.sqlalchemy:SQLAlchemyStorage