From 229043fc42d66f2c162ecd13d4ffcd5a5358411c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Albert?= Date: Tue, 17 Mar 2015 16:00:22 +0100 Subject: [PATCH] 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 --- README.rst | 12 +- cloudkitty/api/v1/controllers/__init__.py | 5 +- cloudkitty/api/v1/controllers/billing.py | 148 +------- cloudkitty/api/v1/controllers/rating.py | 163 ++++++++ cloudkitty/api/v1/controllers/storage.py | 4 +- cloudkitty/api/v1/datamodels/billing.py | 89 +---- cloudkitty/api/v1/datamodels/rating.py | 100 +++++ cloudkitty/api/v1/datamodels/storage.py | 8 +- cloudkitty/billing/__init__.py | 103 +----- cloudkitty/billing/hash/__init__.py | 164 +-------- .../billing/hash/controllers/__init__.py | 31 ++ cloudkitty/billing/hash/controllers/field.py | 84 +---- cloudkitty/billing/hash/controllers/group.py | 101 +---- .../billing/hash/controllers/mapping.py | 147 +------- cloudkitty/billing/hash/controllers/root.py | 32 +- .../billing/hash/controllers/service.py | 78 +--- .../billing/hash/datamodels/__init__.py | 31 ++ cloudkitty/billing/hash/datamodels/field.py | 45 +-- cloudkitty/billing/hash/datamodels/group.py | 42 +-- cloudkitty/billing/hash/datamodels/mapping.py | 66 +--- cloudkitty/billing/hash/datamodels/service.py | 39 +- cloudkitty/billing/hash/db/__init__.py | 31 ++ cloudkitty/billing/hash/db/api.py | 270 +------------- .../billing/hash/db/sqlalchemy/__init__.py | 31 ++ cloudkitty/billing/hash/db/sqlalchemy/api.py | 331 +---------------- .../billing/hash/db/sqlalchemy/migration.py | 31 +- .../billing/hash/db/sqlalchemy/models.py | 197 +--------- cloudkitty/billing/noop.py | 28 +- cloudkitty/cli/dbsync.py | 17 +- cloudkitty/collector/__init__.py | 2 +- cloudkitty/extension_manager.py | 2 +- cloudkitty/orchestrator.py | 24 +- cloudkitty/rating/__init__.py | 103 ++++++ cloudkitty/rating/hash/__init__.py | 177 +++++++++ .../hash/controllers}/__init__.py | 0 cloudkitty/rating/hash/controllers/field.py | 100 +++++ cloudkitty/rating/hash/controllers/group.py | 117 ++++++ cloudkitty/rating/hash/controllers/mapping.py | 163 ++++++++ cloudkitty/rating/hash/controllers/root.py | 48 +++ cloudkitty/rating/hash/controllers/service.py | 94 +++++ cloudkitty/rating/hash/datamodels/__init__.py | 0 cloudkitty/rating/hash/datamodels/field.py | 61 +++ cloudkitty/rating/hash/datamodels/group.py | 58 +++ cloudkitty/rating/hash/datamodels/mapping.py | 82 +++++ cloudkitty/rating/hash/datamodels/service.py | 55 +++ cloudkitty/rating/hash/db/__init__.py | 0 cloudkitty/rating/hash/db/api.py | 286 +++++++++++++++ .../rating/hash/db/sqlalchemy/__init__.py | 0 .../hash/db/sqlalchemy/alembic/__init__.py | 0 .../hash/db/sqlalchemy/alembic/env.py | 2 +- .../hash/db/sqlalchemy/alembic/script.py.mako | 0 .../3dd7e13527f3_initial_migration.py | 0 cloudkitty/rating/hash/db/sqlalchemy/api.py | 347 ++++++++++++++++++ .../rating/hash/db/sqlalchemy/migration.py | 47 +++ .../rating/hash/db/sqlalchemy/models.py | 213 +++++++++++ cloudkitty/rating/noop.py | 44 +++ cloudkitty/storage/sqlalchemy/__init__.py | 4 +- cloudkitty/storage/sqlalchemy/models.py | 2 +- cloudkitty/tests/test_hashmap.py | 26 +- cloudkitty/write_orchestrator.py | 2 +- cloudkitty/writer/__init__.py | 2 +- doc/source/arch.rst | 29 +- doc/source/index.rst | 2 +- doc/source/webapi/billing/hashmap.rst | 42 --- doc/source/webapi/rating/hashmap.rst | 42 +++ doc/source/webapi/v1.rst | 20 +- etc/cloudkitty/cloudkitty.conf.sample | 2 +- setup.cfg | 8 +- 68 files changed, 2564 insertions(+), 2070 deletions(-) create mode 100644 cloudkitty/api/v1/controllers/rating.py create mode 100644 cloudkitty/api/v1/datamodels/rating.py create mode 100644 cloudkitty/rating/__init__.py create mode 100644 cloudkitty/rating/hash/__init__.py rename cloudkitty/{billing/hash/db/sqlalchemy/alembic => rating/hash/controllers}/__init__.py (100%) create mode 100644 cloudkitty/rating/hash/controllers/field.py create mode 100644 cloudkitty/rating/hash/controllers/group.py create mode 100644 cloudkitty/rating/hash/controllers/mapping.py create mode 100644 cloudkitty/rating/hash/controllers/root.py create mode 100644 cloudkitty/rating/hash/controllers/service.py create mode 100644 cloudkitty/rating/hash/datamodels/__init__.py create mode 100644 cloudkitty/rating/hash/datamodels/field.py create mode 100644 cloudkitty/rating/hash/datamodels/group.py create mode 100644 cloudkitty/rating/hash/datamodels/mapping.py create mode 100644 cloudkitty/rating/hash/datamodels/service.py create mode 100644 cloudkitty/rating/hash/db/__init__.py create mode 100644 cloudkitty/rating/hash/db/api.py create mode 100644 cloudkitty/rating/hash/db/sqlalchemy/__init__.py create mode 100644 cloudkitty/rating/hash/db/sqlalchemy/alembic/__init__.py rename cloudkitty/{billing => rating}/hash/db/sqlalchemy/alembic/env.py (93%) rename cloudkitty/{billing => rating}/hash/db/sqlalchemy/alembic/script.py.mako (100%) rename cloudkitty/{billing => rating}/hash/db/sqlalchemy/alembic/versions/3dd7e13527f3_initial_migration.py (100%) create mode 100644 cloudkitty/rating/hash/db/sqlalchemy/api.py create mode 100644 cloudkitty/rating/hash/db/sqlalchemy/migration.py create mode 100644 cloudkitty/rating/hash/db/sqlalchemy/models.py create mode 100644 cloudkitty/rating/noop.py delete mode 100644 doc/source/webapi/billing/hashmap.rst create mode 100644 doc/source/webapi/rating/hashmap.rst diff --git a/README.rst b/README.rst index 6b9a6da4..d463e339 100644 --- a/README.rst +++ b/README.rst @@ -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 diff --git a/cloudkitty/api/v1/controllers/__init__.py b/cloudkitty/api/v1/controllers/__init__.py index 4f33713a..0041ad17 100644 --- a/cloudkitty/api/v1/controllers/__init__.py +++ b/cloudkitty/api/v1/controllers/__init__.py @@ -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() diff --git a/cloudkitty/api/v1/controllers/billing.py b/cloudkitty/api/v1/controllers/billing.py index f4a2ddb4..21395788 100644 --- a/cloudkitty/api/v1/controllers/billing.py +++ b/cloudkitty/api/v1/controllers/billing.py @@ -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 diff --git a/cloudkitty/api/v1/controllers/rating.py b/cloudkitty/api/v1/controllers/rating.py new file mode 100644 index 00000000..b46458ef --- /dev/null +++ b/cloudkitty/api/v1/controllers/rating.py @@ -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 diff --git a/cloudkitty/api/v1/controllers/storage.py b/cloudkitty/api/v1/controllers/storage.py index d48beeee..b7b69f17 100644 --- a/cloudkitty/api/v1/controllers/storage.py +++ b/cloudkitty/api/v1/controllers/storage.py @@ -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']), diff --git a/cloudkitty/api/v1/datamodels/billing.py b/cloudkitty/api/v1/datamodels/billing.py index a298d470..2c1c476d 100644 --- a/cloudkitty/api/v1/datamodels/billing.py +++ b/cloudkitty/api/v1/datamodels/billing.py @@ -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() diff --git a/cloudkitty/api/v1/datamodels/rating.py b/cloudkitty/api/v1/datamodels/rating.py new file mode 100644 index 00000000..0e5d6866 --- /dev/null +++ b/cloudkitty/api/v1/datamodels/rating.py @@ -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] diff --git a/cloudkitty/api/v1/datamodels/storage.py b/cloudkitty/api/v1/datamodels/storage.py index 0c968f7a..37be3936 100644 --- a/cloudkitty/api/v1/datamodels/storage.py +++ b/cloudkitty/api/v1/datamodels/storage.py @@ -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 diff --git a/cloudkitty/billing/__init__.py b/cloudkitty/billing/__init__.py index 1e556ad1..8e682254 100644 --- a/cloudkitty/billing/__init__.py +++ b/cloudkitty/billing/__init__.py @@ -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) diff --git a/cloudkitty/billing/hash/__init__.py b/cloudkitty/billing/hash/__init__.py index 2f1e3084..6bdbe83d 100644 --- a/cloudkitty/billing/hash/__init__.py +++ b/cloudkitty/billing/hash/__init__.py @@ -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() diff --git a/cloudkitty/billing/hash/controllers/__init__.py b/cloudkitty/billing/hash/controllers/__init__.py index e69de29b..ea8c2df3 100644 --- a/cloudkitty/billing/hash/controllers/__init__.py +++ b/cloudkitty/billing/hash/controllers/__init__.py @@ -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() diff --git a/cloudkitty/billing/hash/controllers/field.py b/cloudkitty/billing/hash/controllers/field.py index 245b44c1..53a1d466 100644 --- a/cloudkitty/billing/hash/controllers/field.py +++ b/cloudkitty/billing/hash/controllers/field.py @@ -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 diff --git a/cloudkitty/billing/hash/controllers/group.py b/cloudkitty/billing/hash/controllers/group.py index 5b69a101..4fd894bd 100644 --- a/cloudkitty/billing/hash/controllers/group.py +++ b/cloudkitty/billing/hash/controllers/group.py @@ -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 diff --git a/cloudkitty/billing/hash/controllers/mapping.py b/cloudkitty/billing/hash/controllers/mapping.py index 350f98ea..26194615 100644 --- a/cloudkitty/billing/hash/controllers/mapping.py +++ b/cloudkitty/billing/hash/controllers/mapping.py @@ -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 diff --git a/cloudkitty/billing/hash/controllers/root.py b/cloudkitty/billing/hash/controllers/root.py index 324aae90..4785d2b4 100644 --- a/cloudkitty/billing/hash/controllers/root.py +++ b/cloudkitty/billing/hash/controllers/root.py @@ -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 diff --git a/cloudkitty/billing/hash/controllers/service.py b/cloudkitty/billing/hash/controllers/service.py index 4e88dd83..38df1708 100644 --- a/cloudkitty/billing/hash/controllers/service.py +++ b/cloudkitty/billing/hash/controllers/service.py @@ -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 diff --git a/cloudkitty/billing/hash/datamodels/__init__.py b/cloudkitty/billing/hash/datamodels/__init__.py index e69de29b..768d668f 100644 --- a/cloudkitty/billing/hash/datamodels/__init__.py +++ b/cloudkitty/billing/hash/datamodels/__init__.py @@ -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() diff --git a/cloudkitty/billing/hash/datamodels/field.py b/cloudkitty/billing/hash/datamodels/field.py index aedb6a5c..59642b30 100644 --- a/cloudkitty/billing/hash/datamodels/field.py +++ b/cloudkitty/billing/hash/datamodels/field.py @@ -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 diff --git a/cloudkitty/billing/hash/datamodels/group.py b/cloudkitty/billing/hash/datamodels/group.py index 00942ae2..b91eca4e 100644 --- a/cloudkitty/billing/hash/datamodels/group.py +++ b/cloudkitty/billing/hash/datamodels/group.py @@ -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 diff --git a/cloudkitty/billing/hash/datamodels/mapping.py b/cloudkitty/billing/hash/datamodels/mapping.py index f0fd890a..b55c6790 100644 --- a/cloudkitty/billing/hash/datamodels/mapping.py +++ b/cloudkitty/billing/hash/datamodels/mapping.py @@ -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 diff --git a/cloudkitty/billing/hash/datamodels/service.py b/cloudkitty/billing/hash/datamodels/service.py index 829bebd0..74c1face 100644 --- a/cloudkitty/billing/hash/datamodels/service.py +++ b/cloudkitty/billing/hash/datamodels/service.py @@ -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 diff --git a/cloudkitty/billing/hash/db/__init__.py b/cloudkitty/billing/hash/db/__init__.py index e69de29b..68e8a560 100644 --- a/cloudkitty/billing/hash/db/__init__.py +++ b/cloudkitty/billing/hash/db/__init__.py @@ -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() diff --git a/cloudkitty/billing/hash/db/api.py b/cloudkitty/billing/hash/db/api.py index c54b5a66..050383c3 100644 --- a/cloudkitty/billing/hash/db/api.py +++ b/cloudkitty/billing/hash/db/api.py @@ -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 diff --git a/cloudkitty/billing/hash/db/sqlalchemy/__init__.py b/cloudkitty/billing/hash/db/sqlalchemy/__init__.py index e69de29b..6a66454f 100644 --- a/cloudkitty/billing/hash/db/sqlalchemy/__init__.py +++ b/cloudkitty/billing/hash/db/sqlalchemy/__init__.py @@ -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() diff --git a/cloudkitty/billing/hash/db/sqlalchemy/api.py b/cloudkitty/billing/hash/db/sqlalchemy/api.py index 744c097f..2cb0e751 100644 --- a/cloudkitty/billing/hash/db/sqlalchemy/api.py +++ b/cloudkitty/billing/hash/db/sqlalchemy/api.py @@ -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 diff --git a/cloudkitty/billing/hash/db/sqlalchemy/migration.py b/cloudkitty/billing/hash/db/sqlalchemy/migration.py index a6e16056..e480a5fa 100644 --- a/cloudkitty/billing/hash/db/sqlalchemy/migration.py +++ b/cloudkitty/billing/hash/db/sqlalchemy/migration.py @@ -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 diff --git a/cloudkitty/billing/hash/db/sqlalchemy/models.py b/cloudkitty/billing/hash/db/sqlalchemy/models.py index fcad75a8..c42d251a 100644 --- a/cloudkitty/billing/hash/db/sqlalchemy/models.py +++ b/cloudkitty/billing/hash/db/sqlalchemy/models.py @@ -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 ('').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 ('').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 ('').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 ('').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 diff --git a/cloudkitty/billing/noop.py b/cloudkitty/billing/noop.py index 87c6ecd5..642f5ee9 100644 --- a/cloudkitty/billing/noop.py +++ b/cloudkitty/billing/noop.py @@ -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 diff --git a/cloudkitty/cli/dbsync.py b/cloudkitty/cli/dbsync.py index b564f329..b435251c 100644 --- a/cloudkitty/cli/dbsync.py +++ b/cloudkitty/cli/dbsync.py @@ -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)] diff --git a/cloudkitty/collector/__init__.py b/cloudkitty/collector/__init__.py index 1026bd32..2f998174 100644 --- a/cloudkitty/collector/__init__.py +++ b/cloudkitty/collector/__init__.py @@ -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.'), diff --git a/cloudkitty/extension_manager.py b/cloudkitty/extension_manager.py index 211200d7..5fc65f8a 100644 --- a/cloudkitty/extension_manager.py +++ b/cloudkitty/extension_manager.py @@ -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. diff --git a/cloudkitty/orchestrator.py b/cloudkitty/orchestrator.py index 838a1a32..c52eb0f0 100644 --- a/cloudkitty/orchestrator.py +++ b/cloudkitty/orchestrator.py @@ -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): diff --git a/cloudkitty/rating/__init__.py b/cloudkitty/rating/__init__.py new file mode 100644 index 00000000..b73f16bf --- /dev/null +++ b/cloudkitty/rating/__init__.py @@ -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) diff --git a/cloudkitty/rating/hash/__init__.py b/cloudkitty/rating/hash/__init__.py new file mode 100644 index 00000000..1b892253 --- /dev/null +++ b/cloudkitty/rating/hash/__init__.py @@ -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 diff --git a/cloudkitty/billing/hash/db/sqlalchemy/alembic/__init__.py b/cloudkitty/rating/hash/controllers/__init__.py similarity index 100% rename from cloudkitty/billing/hash/db/sqlalchemy/alembic/__init__.py rename to cloudkitty/rating/hash/controllers/__init__.py diff --git a/cloudkitty/rating/hash/controllers/field.py b/cloudkitty/rating/hash/controllers/field.py new file mode 100644 index 00000000..2e7809c4 --- /dev/null +++ b/cloudkitty/rating/hash/controllers/field.py @@ -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)) diff --git a/cloudkitty/rating/hash/controllers/group.py b/cloudkitty/rating/hash/controllers/group.py new file mode 100644 index 00000000..1dd22592 --- /dev/null +++ b/cloudkitty/rating/hash/controllers/group.py @@ -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)) diff --git a/cloudkitty/rating/hash/controllers/mapping.py b/cloudkitty/rating/hash/controllers/mapping.py new file mode 100644 index 00000000..d7f47d5e --- /dev/null +++ b/cloudkitty/rating/hash/controllers/mapping.py @@ -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)) diff --git a/cloudkitty/rating/hash/controllers/root.py b/cloudkitty/rating/hash/controllers/root.py new file mode 100644 index 00000000..1ae160c6 --- /dev/null +++ b/cloudkitty/rating/hash/controllers/root.py @@ -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 diff --git a/cloudkitty/rating/hash/controllers/service.py b/cloudkitty/rating/hash/controllers/service.py new file mode 100644 index 00000000..3d70870d --- /dev/null +++ b/cloudkitty/rating/hash/controllers/service.py @@ -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)) diff --git a/cloudkitty/rating/hash/datamodels/__init__.py b/cloudkitty/rating/hash/datamodels/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/cloudkitty/rating/hash/datamodels/field.py b/cloudkitty/rating/hash/datamodels/field.py new file mode 100644 index 00000000..aedb6a5c --- /dev/null +++ b/cloudkitty/rating/hash/datamodels/field.py @@ -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]) diff --git a/cloudkitty/rating/hash/datamodels/group.py b/cloudkitty/rating/hash/datamodels/group.py new file mode 100644 index 00000000..00942ae2 --- /dev/null +++ b/cloudkitty/rating/hash/datamodels/group.py @@ -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]) diff --git a/cloudkitty/rating/hash/datamodels/mapping.py b/cloudkitty/rating/hash/datamodels/mapping.py new file mode 100644 index 00000000..f0fd890a --- /dev/null +++ b/cloudkitty/rating/hash/datamodels/mapping.py @@ -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]) diff --git a/cloudkitty/rating/hash/datamodels/service.py b/cloudkitty/rating/hash/datamodels/service.py new file mode 100644 index 00000000..829bebd0 --- /dev/null +++ b/cloudkitty/rating/hash/datamodels/service.py @@ -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]) diff --git a/cloudkitty/rating/hash/db/__init__.py b/cloudkitty/rating/hash/db/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/cloudkitty/rating/hash/db/api.py b/cloudkitty/rating/hash/db/api.py new file mode 100644 index 00000000..17401a1f --- /dev/null +++ b/cloudkitty/rating/hash/db/api.py @@ -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. + """ diff --git a/cloudkitty/rating/hash/db/sqlalchemy/__init__.py b/cloudkitty/rating/hash/db/sqlalchemy/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/cloudkitty/rating/hash/db/sqlalchemy/alembic/__init__.py b/cloudkitty/rating/hash/db/sqlalchemy/alembic/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/cloudkitty/billing/hash/db/sqlalchemy/alembic/env.py b/cloudkitty/rating/hash/db/sqlalchemy/alembic/env.py similarity index 93% rename from cloudkitty/billing/hash/db/sqlalchemy/alembic/env.py rename to cloudkitty/rating/hash/db/sqlalchemy/alembic/env.py index 7ada6cb2..84d29dc3 100644 --- a/cloudkitty/billing/hash/db/sqlalchemy/alembic/env.py +++ b/cloudkitty/rating/hash/db/sqlalchemy/alembic/env.py @@ -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' diff --git a/cloudkitty/billing/hash/db/sqlalchemy/alembic/script.py.mako b/cloudkitty/rating/hash/db/sqlalchemy/alembic/script.py.mako similarity index 100% rename from cloudkitty/billing/hash/db/sqlalchemy/alembic/script.py.mako rename to cloudkitty/rating/hash/db/sqlalchemy/alembic/script.py.mako diff --git a/cloudkitty/billing/hash/db/sqlalchemy/alembic/versions/3dd7e13527f3_initial_migration.py b/cloudkitty/rating/hash/db/sqlalchemy/alembic/versions/3dd7e13527f3_initial_migration.py similarity index 100% rename from cloudkitty/billing/hash/db/sqlalchemy/alembic/versions/3dd7e13527f3_initial_migration.py rename to cloudkitty/rating/hash/db/sqlalchemy/alembic/versions/3dd7e13527f3_initial_migration.py diff --git a/cloudkitty/rating/hash/db/sqlalchemy/api.py b/cloudkitty/rating/hash/db/sqlalchemy/api.py new file mode 100644 index 00000000..43a59bd9 --- /dev/null +++ b/cloudkitty/rating/hash/db/sqlalchemy/api.py @@ -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) diff --git a/cloudkitty/rating/hash/db/sqlalchemy/migration.py b/cloudkitty/rating/hash/db/sqlalchemy/migration.py new file mode 100644 index 00000000..a6e16056 --- /dev/null +++ b/cloudkitty/rating/hash/db/sqlalchemy/migration.py @@ -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) diff --git a/cloudkitty/rating/hash/db/sqlalchemy/models.py b/cloudkitty/rating/hash/db/sqlalchemy/models.py new file mode 100644 index 00000000..fcad75a8 --- /dev/null +++ b/cloudkitty/rating/hash/db/sqlalchemy/models.py @@ -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 ('').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 ('').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 ('').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 ('').format( + uuid=self.mapping_id, + map_type=self.map_type, + value=self.value, + cost=self.cost) diff --git a/cloudkitty/rating/noop.py b/cloudkitty/rating/noop.py new file mode 100644 index 00000000..0a71ded5 --- /dev/null +++ b/cloudkitty/rating/noop.py @@ -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 diff --git a/cloudkitty/storage/sqlalchemy/__init__.py b/cloudkitty/storage/sqlalchemy/__init__.py index eeb8a1c2..11f00082 100644 --- a/cloudkitty/storage/sqlalchemy/__init__.py +++ b/cloudkitty/storage/sqlalchemy/__init__.py @@ -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), diff --git a/cloudkitty/storage/sqlalchemy/models.py b/cloudkitty/storage/sqlalchemy/models.py index 98d182c1..f517170c 100644 --- a/cloudkitty/storage/sqlalchemy/models.py +++ b/cloudkitty/storage/sqlalchemy/models.py @@ -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 diff --git a/cloudkitty/tests/test_hashmap.py b/cloudkitty/tests/test_hashmap.py index b46f75e4..0c394fec 100644 --- a/cloudkitty/tests/test_hashmap.py +++ b/cloudkitty/tests/test_hashmap.py @@ -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) diff --git a/cloudkitty/write_orchestrator.py b/cloudkitty/write_orchestrator.py index 05f76356..1daf3cb4 100644 --- a/cloudkitty/write_orchestrator.py +++ b/cloudkitty/write_orchestrator.py @@ -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) diff --git a/cloudkitty/writer/__init__.py b/cloudkitty/writer/__init__.py index 6e2c0307..8252679e 100644 --- a/cloudkitty/writer/__init__.py +++ b/cloudkitty/writer/__init__.py @@ -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 diff --git a/doc/source/arch.rst b/doc/source/arch.rst index 3a883e1d..0dda63d8 100644 --- a/doc/source/arch.rst +++ b/doc/source/arch.rst @@ -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 diff --git a/doc/source/index.rst b/doc/source/index.rst index 5adf05bf..df5c584b 100644 --- a/doc/source/index.rst +++ b/doc/source/index.rst @@ -48,7 +48,7 @@ Modules :maxdepth: 1 :glob: - webapi/billing/* + webapi/rating/* Indices and tables diff --git a/doc/source/webapi/billing/hashmap.rst b/doc/source/webapi/billing/hashmap.rst deleted file mode 100644 index 54a73895..00000000 --- a/doc/source/webapi/billing/hashmap.rst +++ /dev/null @@ -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: diff --git a/doc/source/webapi/rating/hashmap.rst b/doc/source/webapi/rating/hashmap.rst new file mode 100644 index 00000000..6afeadc6 --- /dev/null +++ b/doc/source/webapi/rating/hashmap.rst @@ -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: diff --git a/doc/source/webapi/v1.rst b/doc/source/webapi/v1.rst index 4c579a83..06fc9830 100644 --- a/doc/source/webapi/v1.rst +++ b/doc/source/webapi/v1.rst @@ -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: diff --git a/etc/cloudkitty/cloudkitty.conf.sample b/etc/cloudkitty/cloudkitty.conf.sample index d2267647..a2379fec 100644 --- a/etc/cloudkitty/cloudkitty.conf.sample +++ b/etc/cloudkitty/cloudkitty.conf.sample @@ -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) diff --git a/setup.cfg b/setup.cfg index 0fdeed95..755f2a25 100644 --- a/setup.cfg +++ b/setup.cfg @@ -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