Renaming billing to rating
As CloudKitty is a rating and not a billing component, we needed to rename all components and change paths on the API. We've created a compatibility layer to help people with the migration. The old API objects are now deprecated and shouldn't be used, they will be removed in a near future. Change-Id: I9e264f4ed01f4c94366eb51f612d634312b6a2f7
This commit is contained in:
parent
ab2a6bff5a
commit
229043fc42
12
README.rst
12
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
|
||||
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -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']),
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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]
|
|
@ -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
|
||||
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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()
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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()
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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()
|
|
@ -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
|
||||
|
|
|
@ -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()
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -15,199 +15,4 @@
|
|||
#
|
||||
# @author: Stéphane Albert
|
||||
#
|
||||
from oslo.db.sqlalchemy import models
|
||||
import sqlalchemy
|
||||
from sqlalchemy.ext import declarative
|
||||
from sqlalchemy import orm
|
||||
from sqlalchemy import schema
|
||||
|
||||
Base = declarative.declarative_base()
|
||||
|
||||
|
||||
class HashMapBase(models.ModelBase):
|
||||
__table_args__ = {'mysql_charset': "utf8",
|
||||
'mysql_engine': "InnoDB"}
|
||||
fk_to_resolve = {}
|
||||
|
||||
def save(self, session=None):
|
||||
from cloudkitty import db
|
||||
|
||||
if session is None:
|
||||
session = db.get_session()
|
||||
|
||||
super(HashMapBase, self).save(session=session)
|
||||
|
||||
def as_dict(self):
|
||||
d = {}
|
||||
for c in self.__table__.columns:
|
||||
if c.name == 'id':
|
||||
continue
|
||||
d[c.name] = self[c.name]
|
||||
return d
|
||||
|
||||
def _recursive_resolve(self, path):
|
||||
obj = self
|
||||
for attr in path.split('.'):
|
||||
if hasattr(obj, attr):
|
||||
obj = getattr(obj, attr)
|
||||
else:
|
||||
return None
|
||||
return obj
|
||||
|
||||
def export_model(self):
|
||||
res = self.as_dict()
|
||||
for fk, mapping in self.fk_to_resolve.items():
|
||||
res[fk] = self._recursive_resolve(mapping)
|
||||
return res
|
||||
|
||||
|
||||
class HashMapService(Base, HashMapBase):
|
||||
"""An hashmap service.
|
||||
|
||||
"""
|
||||
__tablename__ = 'hashmap_services'
|
||||
|
||||
id = sqlalchemy.Column(sqlalchemy.Integer,
|
||||
primary_key=True)
|
||||
service_id = sqlalchemy.Column(sqlalchemy.String(36),
|
||||
nullable=False,
|
||||
unique=True)
|
||||
name = sqlalchemy.Column(
|
||||
sqlalchemy.String(255),
|
||||
nullable=False,
|
||||
unique=True
|
||||
)
|
||||
fields = orm.relationship('HashMapField',
|
||||
backref=orm.backref(
|
||||
'service',
|
||||
lazy='immediate'))
|
||||
mappings = orm.relationship('HashMapMapping',
|
||||
backref=orm.backref(
|
||||
'service',
|
||||
lazy='immediate'))
|
||||
|
||||
def __repr__(self):
|
||||
return ('<HashMapService[{uuid}]: '
|
||||
'service={service}>').format(
|
||||
uuid=self.service_id,
|
||||
service=self.name)
|
||||
|
||||
|
||||
class HashMapField(Base, HashMapBase):
|
||||
"""An hashmap field.
|
||||
|
||||
"""
|
||||
__tablename__ = 'hashmap_fields'
|
||||
fk_to_resolve = {'service_id': 'service.service_id'}
|
||||
|
||||
@declarative.declared_attr
|
||||
def __table_args__(cls):
|
||||
args = (schema.UniqueConstraint('field_id', 'name',
|
||||
name='uniq_field'),
|
||||
schema.UniqueConstraint('service_id', 'name',
|
||||
name='uniq_map_service_field'),
|
||||
HashMapBase.__table_args__,)
|
||||
return args
|
||||
|
||||
id = sqlalchemy.Column(sqlalchemy.Integer,
|
||||
primary_key=True)
|
||||
field_id = sqlalchemy.Column(sqlalchemy.String(36),
|
||||
nullable=False,
|
||||
unique=True)
|
||||
name = sqlalchemy.Column(sqlalchemy.String(255),
|
||||
nullable=False)
|
||||
service_id = sqlalchemy.Column(
|
||||
sqlalchemy.Integer,
|
||||
sqlalchemy.ForeignKey('hashmap_services.id',
|
||||
ondelete='CASCADE'),
|
||||
nullable=False
|
||||
)
|
||||
mappings = orm.relationship('HashMapMapping',
|
||||
backref=orm.backref(
|
||||
'field',
|
||||
lazy='immediate'))
|
||||
|
||||
def __repr__(self):
|
||||
return ('<HashMapField[{uuid}]: '
|
||||
'field={field}>').format(
|
||||
uuid=self.field_id,
|
||||
field=self.name)
|
||||
|
||||
|
||||
class HashMapGroup(Base, HashMapBase):
|
||||
"""A grouping of hashmap calculations.
|
||||
|
||||
"""
|
||||
__tablename__ = 'hashmap_groups'
|
||||
|
||||
id = sqlalchemy.Column(sqlalchemy.Integer,
|
||||
primary_key=True)
|
||||
group_id = sqlalchemy.Column(sqlalchemy.String(36),
|
||||
nullable=False,
|
||||
unique=True)
|
||||
name = sqlalchemy.Column(sqlalchemy.String(255),
|
||||
nullable=False,
|
||||
unique=True)
|
||||
mappings = orm.relationship('HashMapMapping',
|
||||
backref=orm.backref(
|
||||
'group',
|
||||
lazy='immediate'))
|
||||
|
||||
def __repr__(self):
|
||||
return ('<HashMapGroup[{uuid}]: '
|
||||
'name={name}>').format(
|
||||
uuid=self.group_id,
|
||||
name=self.name)
|
||||
|
||||
|
||||
class HashMapMapping(Base, HashMapBase):
|
||||
"""A mapping between a field a value and a type.
|
||||
|
||||
"""
|
||||
__tablename__ = 'hashmap_maps'
|
||||
fk_to_resolve = {'service_id': 'service.service_id',
|
||||
'field_id': 'field.field_id',
|
||||
'group_id': 'group.group_id'}
|
||||
|
||||
@declarative.declared_attr
|
||||
def __table_args__(cls):
|
||||
args = (schema.UniqueConstraint('value', 'field_id',
|
||||
name='uniq_field_mapping'),
|
||||
schema.UniqueConstraint('value', 'service_id',
|
||||
name='uniq_service_mapping'),
|
||||
HashMapBase.__table_args__,)
|
||||
return args
|
||||
|
||||
id = sqlalchemy.Column(sqlalchemy.Integer,
|
||||
primary_key=True)
|
||||
mapping_id = sqlalchemy.Column(sqlalchemy.String(36),
|
||||
nullable=False,
|
||||
unique=True)
|
||||
value = sqlalchemy.Column(sqlalchemy.String(255),
|
||||
nullable=True)
|
||||
cost = sqlalchemy.Column(sqlalchemy.Numeric(20, 8),
|
||||
nullable=False)
|
||||
map_type = sqlalchemy.Column(sqlalchemy.Enum('flat',
|
||||
'rate',
|
||||
name='enum_map_type'),
|
||||
nullable=False)
|
||||
service_id = sqlalchemy.Column(sqlalchemy.Integer,
|
||||
sqlalchemy.ForeignKey('hashmap_services.id',
|
||||
ondelete='CASCADE'),
|
||||
nullable=True)
|
||||
field_id = sqlalchemy.Column(sqlalchemy.Integer,
|
||||
sqlalchemy.ForeignKey('hashmap_fields.id',
|
||||
ondelete='CASCADE'),
|
||||
nullable=True)
|
||||
group_id = sqlalchemy.Column(sqlalchemy.Integer,
|
||||
sqlalchemy.ForeignKey('hashmap_groups.id',
|
||||
ondelete='SET NULL'),
|
||||
nullable=True)
|
||||
|
||||
def __repr__(self):
|
||||
return ('<HashMapMapping[{uuid}]: '
|
||||
'type={map_type} {value}={cost}>').format(
|
||||
uuid=self.mapping_id,
|
||||
map_type=self.map_type,
|
||||
value=self.value,
|
||||
cost=self.cost)
|
||||
from cloudkitty.rating.hash.db.api.sqlalchemy.models import * # noqa
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)]
|
||||
|
|
|
@ -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.'),
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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)
|
|
@ -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
|
|
@ -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))
|
|
@ -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))
|
|
@ -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))
|
|
@ -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
|
|
@ -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))
|
|
@ -0,0 +1,61 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2015 Objectif Libre
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
#
|
||||
# @author: Stéphane Albert
|
||||
#
|
||||
from wsme import types as wtypes
|
||||
|
||||
from cloudkitty.api.v1 import types as ck_types
|
||||
|
||||
|
||||
class Field(wtypes.Base):
|
||||
"""Type describing a field.
|
||||
|
||||
A field is mapping a value of the 'desc' dict of the CloudKitty data. It's
|
||||
used to map the name of a metadata.
|
||||
"""
|
||||
|
||||
field_id = wtypes.wsattr(ck_types.UuidType(),
|
||||
mandatory=False,
|
||||
readonly=True)
|
||||
"""UUID of the field."""
|
||||
|
||||
name = wtypes.wsattr(wtypes.text, mandatory=True)
|
||||
"""Name of the field."""
|
||||
|
||||
service_id = wtypes.wsattr(ck_types.UuidType(),
|
||||
mandatory=True)
|
||||
"""UUID of the parent service."""
|
||||
|
||||
@classmethod
|
||||
def sample(cls):
|
||||
sample = cls(field_id='ac55b000-a05b-4832-b2ff-265a034886ab',
|
||||
name='image_id',
|
||||
service_id='a733d0e1-1ec9-4800-8df8-671e4affd017')
|
||||
return sample
|
||||
|
||||
|
||||
class FieldCollection(wtypes.Base):
|
||||
"""Type describing a list of fields.
|
||||
|
||||
"""
|
||||
|
||||
fields = [Field]
|
||||
"""List of fields."""
|
||||
|
||||
@classmethod
|
||||
def sample(cls):
|
||||
sample = Field.sample()
|
||||
return cls(fields=[sample])
|
|
@ -0,0 +1,58 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2015 Objectif Libre
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
#
|
||||
# @author: Stéphane Albert
|
||||
#
|
||||
from wsme import types as wtypes
|
||||
|
||||
from cloudkitty.api.v1 import types as ck_types
|
||||
|
||||
|
||||
class Group(wtypes.Base):
|
||||
"""Type describing a group.
|
||||
|
||||
A group is used to divide calculations. It can be used to create a group
|
||||
for the instance rating (flavor) and one if we have premium images
|
||||
(image_id). So you can take into account multiple parameters during the
|
||||
rating.
|
||||
"""
|
||||
|
||||
group_id = wtypes.wsattr(ck_types.UuidType(),
|
||||
mandatory=False,
|
||||
readonly=True)
|
||||
"""UUID of the group."""
|
||||
|
||||
name = wtypes.wsattr(wtypes.text, mandatory=True)
|
||||
"""Name of the group."""
|
||||
|
||||
@classmethod
|
||||
def sample(cls):
|
||||
sample = cls(group_id='afe898cb-86d8-4557-ad67-f4f01891bbee',
|
||||
name='instance_rating')
|
||||
return sample
|
||||
|
||||
|
||||
class GroupCollection(wtypes.Base):
|
||||
"""Type describing a list of groups.
|
||||
|
||||
"""
|
||||
|
||||
groups = [Group]
|
||||
"""List of groups."""
|
||||
|
||||
@classmethod
|
||||
def sample(cls):
|
||||
sample = Group.sample()
|
||||
return cls(groups=[sample])
|
|
@ -0,0 +1,82 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2015 Objectif Libre
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
#
|
||||
# @author: Stéphane Albert
|
||||
#
|
||||
import decimal
|
||||
|
||||
from wsme import types as wtypes
|
||||
|
||||
from cloudkitty.api.v1 import types as ck_types
|
||||
|
||||
MAP_TYPE = wtypes.Enum(wtypes.text, 'flat', 'rate')
|
||||
|
||||
|
||||
class Mapping(wtypes.Base):
|
||||
"""Type describing a Mapping.
|
||||
|
||||
A mapping is used to apply rating rules based on a value, if the parent is
|
||||
a field then it's check the value of a metadata. If it's a service then it
|
||||
directly apply the rate to the volume.
|
||||
"""
|
||||
|
||||
mapping_id = wtypes.wsattr(ck_types.UuidType(),
|
||||
mandatory=False,
|
||||
readonly=True)
|
||||
"""UUID of the mapping."""
|
||||
|
||||
value = wtypes.wsattr(wtypes.text, mandatory=False)
|
||||
"""Key of the mapping."""
|
||||
|
||||
map_type = wtypes.wsattr(MAP_TYPE, default='flat', name='type')
|
||||
"""Type of the mapping."""
|
||||
|
||||
cost = wtypes.wsattr(decimal.Decimal, mandatory=True)
|
||||
"""Value of the mapping."""
|
||||
|
||||
service_id = wtypes.wsattr(ck_types.UuidType(),
|
||||
mandatory=False)
|
||||
"""UUID of the service."""
|
||||
|
||||
field_id = wtypes.wsattr(ck_types.UuidType(),
|
||||
mandatory=False)
|
||||
"""UUID of the field."""
|
||||
|
||||
group_id = wtypes.wsattr(ck_types.UuidType(),
|
||||
mandatory=False)
|
||||
"""UUID of the hashmap group."""
|
||||
|
||||
@classmethod
|
||||
def sample(cls):
|
||||
sample = cls(mapping_id='39dbd39d-f663-4444-a795-fb19d81af136',
|
||||
field_id='ac55b000-a05b-4832-b2ff-265a034886ab',
|
||||
value='m1.micro',
|
||||
map_type='flat',
|
||||
cost=decimal.Decimal('4.2'))
|
||||
return sample
|
||||
|
||||
|
||||
class MappingCollection(wtypes.Base):
|
||||
"""Type describing a list of mappings.
|
||||
|
||||
"""
|
||||
|
||||
mappings = [Mapping]
|
||||
"""List of mappings."""
|
||||
|
||||
@classmethod
|
||||
def sample(cls):
|
||||
sample = Mapping.sample()
|
||||
return cls(mappings=[sample])
|
|
@ -0,0 +1,55 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2015 Objectif Libre
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
#
|
||||
# @author: Stéphane Albert
|
||||
#
|
||||
from wsme import types as wtypes
|
||||
|
||||
from cloudkitty.api.v1 import types as ck_types
|
||||
|
||||
|
||||
class Service(wtypes.Base):
|
||||
"""Type describing a service.
|
||||
|
||||
A service is directly mapped to the usage key, the collected service.
|
||||
"""
|
||||
|
||||
service_id = wtypes.wsattr(ck_types.UuidType(),
|
||||
mandatory=False,
|
||||
readonly=True)
|
||||
"""UUID of the service."""
|
||||
|
||||
name = wtypes.wsattr(wtypes.text, mandatory=True)
|
||||
"""Name of the service."""
|
||||
|
||||
@classmethod
|
||||
def sample(cls):
|
||||
sample = cls(service_id='a733d0e1-1ec9-4800-8df8-671e4affd017',
|
||||
name='compute')
|
||||
return sample
|
||||
|
||||
|
||||
class ServiceCollection(wtypes.Base):
|
||||
"""Type describing a list of services.
|
||||
|
||||
"""
|
||||
|
||||
services = [Service]
|
||||
"""List of services."""
|
||||
|
||||
@classmethod
|
||||
def sample(cls):
|
||||
sample = Service.sample()
|
||||
return cls(services=[sample])
|
|
@ -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.
|
||||
"""
|
|
@ -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'
|
|
@ -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)
|
|
@ -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)
|
|
@ -0,0 +1,213 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2014 Objectif Libre
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
#
|
||||
# @author: Stéphane Albert
|
||||
#
|
||||
from oslo.db.sqlalchemy import models
|
||||
import sqlalchemy
|
||||
from sqlalchemy.ext import declarative
|
||||
from sqlalchemy import orm
|
||||
from sqlalchemy import schema
|
||||
|
||||
Base = declarative.declarative_base()
|
||||
|
||||
|
||||
class HashMapBase(models.ModelBase):
|
||||
__table_args__ = {'mysql_charset': "utf8",
|
||||
'mysql_engine': "InnoDB"}
|
||||
fk_to_resolve = {}
|
||||
|
||||
def save(self, session=None):
|
||||
from cloudkitty import db
|
||||
|
||||
if session is None:
|
||||
session = db.get_session()
|
||||
|
||||
super(HashMapBase, self).save(session=session)
|
||||
|
||||
def as_dict(self):
|
||||
d = {}
|
||||
for c in self.__table__.columns:
|
||||
if c.name == 'id':
|
||||
continue
|
||||
d[c.name] = self[c.name]
|
||||
return d
|
||||
|
||||
def _recursive_resolve(self, path):
|
||||
obj = self
|
||||
for attr in path.split('.'):
|
||||
if hasattr(obj, attr):
|
||||
obj = getattr(obj, attr)
|
||||
else:
|
||||
return None
|
||||
return obj
|
||||
|
||||
def export_model(self):
|
||||
res = self.as_dict()
|
||||
for fk, mapping in self.fk_to_resolve.items():
|
||||
res[fk] = self._recursive_resolve(mapping)
|
||||
return res
|
||||
|
||||
|
||||
class HashMapService(Base, HashMapBase):
|
||||
"""An hashmap service.
|
||||
|
||||
"""
|
||||
__tablename__ = 'hashmap_services'
|
||||
|
||||
id = sqlalchemy.Column(sqlalchemy.Integer,
|
||||
primary_key=True)
|
||||
service_id = sqlalchemy.Column(sqlalchemy.String(36),
|
||||
nullable=False,
|
||||
unique=True)
|
||||
name = sqlalchemy.Column(
|
||||
sqlalchemy.String(255),
|
||||
nullable=False,
|
||||
unique=True
|
||||
)
|
||||
fields = orm.relationship('HashMapField',
|
||||
backref=orm.backref(
|
||||
'service',
|
||||
lazy='immediate'))
|
||||
mappings = orm.relationship('HashMapMapping',
|
||||
backref=orm.backref(
|
||||
'service',
|
||||
lazy='immediate'))
|
||||
|
||||
def __repr__(self):
|
||||
return ('<HashMapService[{uuid}]: '
|
||||
'service={service}>').format(
|
||||
uuid=self.service_id,
|
||||
service=self.name)
|
||||
|
||||
|
||||
class HashMapField(Base, HashMapBase):
|
||||
"""An hashmap field.
|
||||
|
||||
"""
|
||||
__tablename__ = 'hashmap_fields'
|
||||
fk_to_resolve = {'service_id': 'service.service_id'}
|
||||
|
||||
@declarative.declared_attr
|
||||
def __table_args__(cls):
|
||||
args = (schema.UniqueConstraint('field_id', 'name',
|
||||
name='uniq_field'),
|
||||
schema.UniqueConstraint('service_id', 'name',
|
||||
name='uniq_map_service_field'),
|
||||
HashMapBase.__table_args__,)
|
||||
return args
|
||||
|
||||
id = sqlalchemy.Column(sqlalchemy.Integer,
|
||||
primary_key=True)
|
||||
field_id = sqlalchemy.Column(sqlalchemy.String(36),
|
||||
nullable=False,
|
||||
unique=True)
|
||||
name = sqlalchemy.Column(sqlalchemy.String(255),
|
||||
nullable=False)
|
||||
service_id = sqlalchemy.Column(
|
||||
sqlalchemy.Integer,
|
||||
sqlalchemy.ForeignKey('hashmap_services.id',
|
||||
ondelete='CASCADE'),
|
||||
nullable=False
|
||||
)
|
||||
mappings = orm.relationship('HashMapMapping',
|
||||
backref=orm.backref(
|
||||
'field',
|
||||
lazy='immediate'))
|
||||
|
||||
def __repr__(self):
|
||||
return ('<HashMapField[{uuid}]: '
|
||||
'field={field}>').format(
|
||||
uuid=self.field_id,
|
||||
field=self.name)
|
||||
|
||||
|
||||
class HashMapGroup(Base, HashMapBase):
|
||||
"""A grouping of hashmap calculations.
|
||||
|
||||
"""
|
||||
__tablename__ = 'hashmap_groups'
|
||||
|
||||
id = sqlalchemy.Column(sqlalchemy.Integer,
|
||||
primary_key=True)
|
||||
group_id = sqlalchemy.Column(sqlalchemy.String(36),
|
||||
nullable=False,
|
||||
unique=True)
|
||||
name = sqlalchemy.Column(sqlalchemy.String(255),
|
||||
nullable=False,
|
||||
unique=True)
|
||||
mappings = orm.relationship('HashMapMapping',
|
||||
backref=orm.backref(
|
||||
'group',
|
||||
lazy='immediate'))
|
||||
|
||||
def __repr__(self):
|
||||
return ('<HashMapGroup[{uuid}]: '
|
||||
'name={name}>').format(
|
||||
uuid=self.group_id,
|
||||
name=self.name)
|
||||
|
||||
|
||||
class HashMapMapping(Base, HashMapBase):
|
||||
"""A mapping between a field a value and a type.
|
||||
|
||||
"""
|
||||
__tablename__ = 'hashmap_maps'
|
||||
fk_to_resolve = {'service_id': 'service.service_id',
|
||||
'field_id': 'field.field_id',
|
||||
'group_id': 'group.group_id'}
|
||||
|
||||
@declarative.declared_attr
|
||||
def __table_args__(cls):
|
||||
args = (schema.UniqueConstraint('value', 'field_id',
|
||||
name='uniq_field_mapping'),
|
||||
schema.UniqueConstraint('value', 'service_id',
|
||||
name='uniq_service_mapping'),
|
||||
HashMapBase.__table_args__,)
|
||||
return args
|
||||
|
||||
id = sqlalchemy.Column(sqlalchemy.Integer,
|
||||
primary_key=True)
|
||||
mapping_id = sqlalchemy.Column(sqlalchemy.String(36),
|
||||
nullable=False,
|
||||
unique=True)
|
||||
value = sqlalchemy.Column(sqlalchemy.String(255),
|
||||
nullable=True)
|
||||
cost = sqlalchemy.Column(sqlalchemy.Numeric(20, 8),
|
||||
nullable=False)
|
||||
map_type = sqlalchemy.Column(sqlalchemy.Enum('flat',
|
||||
'rate',
|
||||
name='enum_map_type'),
|
||||
nullable=False)
|
||||
service_id = sqlalchemy.Column(sqlalchemy.Integer,
|
||||
sqlalchemy.ForeignKey('hashmap_services.id',
|
||||
ondelete='CASCADE'),
|
||||
nullable=True)
|
||||
field_id = sqlalchemy.Column(sqlalchemy.Integer,
|
||||
sqlalchemy.ForeignKey('hashmap_fields.id',
|
||||
ondelete='CASCADE'),
|
||||
nullable=True)
|
||||
group_id = sqlalchemy.Column(sqlalchemy.Integer,
|
||||
sqlalchemy.ForeignKey('hashmap_groups.id',
|
||||
ondelete='SET NULL'),
|
||||
nullable=True)
|
||||
|
||||
def __repr__(self):
|
||||
return ('<HashMapMapping[{uuid}]: '
|
||||
'type={map_type} {value}={cost}>').format(
|
||||
uuid=self.mapping_id,
|
||||
map_type=self.map_type,
|
||||
value=self.value,
|
||||
cost=self.cost)
|
|
@ -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
|
|
@ -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),
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
||||
|
|
|
@ -48,7 +48,7 @@ Modules
|
|||
:maxdepth: 1
|
||||
:glob:
|
||||
|
||||
webapi/billing/*
|
||||
webapi/rating/*
|
||||
|
||||
|
||||
Indices and tables
|
||||
|
|
|
@ -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:
|
|
@ -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:
|
|
@ -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:
|
||||
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue