Implemented HashMap API
Change-Id: Id9f02088b9bbb0da730507029c8c375b8a95531d
This commit is contained in:
parent
2efb97e9f0
commit
204783c2ca
|
@ -1,86 +0,0 @@
|
|||
# -*- 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 json
|
||||
|
||||
from cloudkitty import billing
|
||||
|
||||
|
||||
class BasicHashMapController(billing.BillingController):
|
||||
|
||||
def get_module_info(self):
|
||||
module = BasicHashMap()
|
||||
infos = {
|
||||
'name': 'hashmap',
|
||||
'description': 'Basic hashmap billing module.',
|
||||
'enabled': module.enabled,
|
||||
'hot_config': True,
|
||||
}
|
||||
return infos
|
||||
|
||||
|
||||
class BasicHashMap(billing.BillingProcessorBase):
|
||||
|
||||
controller = BasicHashMapController
|
||||
|
||||
def __init__(self):
|
||||
self._billing_info = {}
|
||||
self._load_billing_rates()
|
||||
|
||||
@property
|
||||
def enabled(self):
|
||||
# TODO(sheeprine): Implement real feature
|
||||
return True
|
||||
|
||||
def _load_billing_rates(self):
|
||||
# FIXME We should use another path
|
||||
self._billing_info = json.loads(open('billing_info.json').read())
|
||||
|
||||
def process_service(self, name, data):
|
||||
if name not in self._billing_info:
|
||||
return
|
||||
serv_b_info = self._billing_info[name]
|
||||
for entry in data:
|
||||
flat = 0
|
||||
rate = 1
|
||||
entry_desc = entry['desc']
|
||||
for field in serv_b_info:
|
||||
if field not in entry_desc:
|
||||
continue
|
||||
b_info = serv_b_info[field]
|
||||
if b_info['type'] == 'rate':
|
||||
if entry_desc[field] in b_info['map']:
|
||||
rate *= b_info['map'][entry_desc[field]]
|
||||
elif 'default' in b_info['map']:
|
||||
rate *= b_info['map']['default']
|
||||
elif b_info['type'] == 'flat':
|
||||
new_flat = 0
|
||||
if entry_desc[field] in b_info['map']:
|
||||
new_flat = b_info['map'][entry_desc[field]]
|
||||
elif 'default' in b_info['map']:
|
||||
new_flat = b_info['map']['default']
|
||||
if new_flat > flat:
|
||||
flat = new_flat
|
||||
billing_info = {'price': flat * rate}
|
||||
entry['billing'] = billing_info
|
||||
|
||||
def process(self, data):
|
||||
for cur_data in data:
|
||||
cur_usage = cur_data['usage']
|
||||
for service in cur_usage:
|
||||
self.process_service(service, cur_usage[service])
|
||||
return data
|
|
@ -0,0 +1,260 @@
|
|||
# -*- 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 routing
|
||||
from wsme import types as wtypes
|
||||
import wsmeext.pecan as wsme_pecan
|
||||
|
||||
from cloudkitty import billing
|
||||
from cloudkitty.billing.hash.db import api
|
||||
from cloudkitty.db import api as db_api
|
||||
from cloudkitty.openstack.common import log as logging
|
||||
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
MAP_TYPE = wtypes.Enum(wtypes.text, 'flat', 'rate')
|
||||
|
||||
|
||||
class Mapping(wtypes.Base):
|
||||
|
||||
map_type = wtypes.wsattr(MAP_TYPE, default='rate', name='type')
|
||||
|
||||
value = wtypes.wsattr(float, mandatory=True)
|
||||
|
||||
|
||||
class BasicHashMapConfigController(billing.BillingConfigController):
|
||||
|
||||
@pecan.expose()
|
||||
def _route(self, args, request=None):
|
||||
if len(args) > 2:
|
||||
# Taken from base _route function
|
||||
if request is None:
|
||||
from pecan import request # noqa
|
||||
method = request.params.get('_method', request.method).lower()
|
||||
if request.method == 'GET' and method in ('delete', 'put'):
|
||||
pecan.abort(405)
|
||||
|
||||
if request.method == 'GET':
|
||||
return routing.lookup_controller(self.get_mapping, args)
|
||||
return super(BasicHashMapConfigController, self)._route(args)
|
||||
|
||||
@wsme_pecan.wsexpose(Mapping, wtypes.text, wtypes.text, wtypes.text)
|
||||
def get_mapping(self, service, field, key):
|
||||
"""Return the list of every mappings.
|
||||
|
||||
"""
|
||||
hashmap = api.get_instance()
|
||||
try:
|
||||
return hashmap.get_mapping(service, field, key)
|
||||
except (api.NoSuchService, api.NoSuchField, api.NoSuchMapping) as e:
|
||||
pecan.abort(400, str(e))
|
||||
|
||||
@wsme_pecan.wsexpose([wtypes.text])
|
||||
def get(self):
|
||||
hashmap = api.get_instance()
|
||||
return [service.name for service in hashmap.list_services()]
|
||||
|
||||
@wsme_pecan.wsexpose([wtypes.text], wtypes.text, wtypes.text)
|
||||
def get_one(self, service=None, field=None):
|
||||
"""Return the list of every sub keys.
|
||||
|
||||
"""
|
||||
hashmap = api.get_instance()
|
||||
if field:
|
||||
try:
|
||||
return [mapping.key for mapping in hashmap.list_mappings(
|
||||
service,
|
||||
field)]
|
||||
except (api.NoSuchService, api.NoSuchField) as e:
|
||||
pecan.abort(400, str(e))
|
||||
|
||||
else:
|
||||
try:
|
||||
return [f.name for f in hashmap.list_fields(service)]
|
||||
except api.NoSuchService as e:
|
||||
pecan.abort(400, str(e))
|
||||
|
||||
# FIXME (sheeprine): Still a problem with our routing and the different
|
||||
# object types. For service/field it's text or a mapping.
|
||||
@wsme_pecan.wsexpose(None, wtypes.text, wtypes.text, wtypes.text,
|
||||
body=Mapping)
|
||||
def post(self, service, field=None, key=None, mapping=None):
|
||||
hashmap = api.get_instance()
|
||||
if field:
|
||||
if key:
|
||||
if mapping:
|
||||
try:
|
||||
# FIXME(sheeprine): We should return the result
|
||||
hashmap.create_mapping(
|
||||
service,
|
||||
field,
|
||||
key,
|
||||
value=mapping.value,
|
||||
map_type=mapping.map_type
|
||||
)
|
||||
pecan.response.headers['Location'] = pecan.request.path
|
||||
except api.MappingAlreadyExists as e:
|
||||
pecan.abort(409, str(e))
|
||||
else:
|
||||
e = ValueError('Mapping can\'t be empty.')
|
||||
pecan.abort(400, str(e))
|
||||
else:
|
||||
try:
|
||||
hashmap.create_field(service, field)
|
||||
pecan.response.headers['Location'] = pecan.request.path
|
||||
except api.FieldAlreadyExists as e:
|
||||
pecan.abort(409, str(e))
|
||||
else:
|
||||
try:
|
||||
hashmap.create_service(service)
|
||||
pecan.response.headers['Location'] = pecan.request.path
|
||||
except api.ServiceAlreadyExists as e:
|
||||
pecan.abort(409, str(e))
|
||||
pecan.response.status = 201
|
||||
|
||||
@wsme_pecan.wsexpose(None, wtypes.text, wtypes.text, wtypes.text,
|
||||
body=Mapping)
|
||||
def put(self, service, field, key, mapping):
|
||||
hashmap = api.get_instance()
|
||||
try:
|
||||
hashmap.update_mapping(
|
||||
service,
|
||||
field,
|
||||
key,
|
||||
value=mapping.value,
|
||||
map_type=mapping.map_type
|
||||
)
|
||||
pecan.response.headers['Location'] = pecan.request.path
|
||||
pecan.response.status = 204
|
||||
except (api.NoSuchService, api.NoSuchField, api.NoSuchMapping) as e:
|
||||
pecan.abort(400, str(e))
|
||||
|
||||
@wsme_pecan.wsexpose(None, wtypes.text, wtypes.text, wtypes.text)
|
||||
def delete(self, service, field=None, key=None):
|
||||
"""Delete the parent and all the sub keys recursively.
|
||||
|
||||
"""
|
||||
hashmap = api.get_instance()
|
||||
try:
|
||||
if field:
|
||||
if key:
|
||||
hashmap.delete_mapping(service, field, key)
|
||||
else:
|
||||
hashmap.delete_field(service, field)
|
||||
else:
|
||||
hashmap.delete_service(service)
|
||||
except (api.NoSuchService, api.NoSuchField, api.NoSuchMapping) as e:
|
||||
pecan.abort(400, str(e))
|
||||
pecan.response.status = 204
|
||||
|
||||
|
||||
class BasicHashMapController(billing.BillingController):
|
||||
|
||||
config = BasicHashMapConfigController()
|
||||
|
||||
def get_module_info(self):
|
||||
module = BasicHashMap()
|
||||
infos = {
|
||||
'name': 'hashmap',
|
||||
'description': 'Basic hashmap billing module.',
|
||||
'enabled': module.enabled,
|
||||
'hot_config': True,
|
||||
}
|
||||
return infos
|
||||
|
||||
|
||||
class BasicHashMap(billing.BillingProcessorBase):
|
||||
|
||||
controller = BasicHashMapController
|
||||
db_api = api.get_instance()
|
||||
|
||||
def __init__(self):
|
||||
self._billing_info = {}
|
||||
self._load_billing_rates()
|
||||
|
||||
@property
|
||||
def enabled(self):
|
||||
"""Check if the module is enabled
|
||||
|
||||
:returns: bool if module is enabled
|
||||
"""
|
||||
# FIXME(sheeprine): Hardcoded values to check the state
|
||||
api = db_api.get_instance()
|
||||
module_db = api.get_module_enable_state()
|
||||
return module_db.get_state('hashmap') or False
|
||||
|
||||
def reload_config(self):
|
||||
self._load_billing_rates()
|
||||
|
||||
def _load_billing_rates(self):
|
||||
self._billing_info = {}
|
||||
hashmap = api.get_instance()
|
||||
services = hashmap.list_services()
|
||||
for service in services:
|
||||
service = service[0]
|
||||
self._billing_info[service] = {}
|
||||
fields = hashmap.list_fields(service)
|
||||
for field in fields:
|
||||
field = field[0]
|
||||
self._billing_info[service][field] = {}
|
||||
mappings = hashmap.list_mappings(service, field)
|
||||
for mapping in mappings:
|
||||
mapping = mapping[0]
|
||||
mapping_db = hashmap.get_mapping(service, field, mapping)
|
||||
map_dict = {}
|
||||
map_dict['value'] = mapping_db.value
|
||||
map_dict['type'] = mapping_db.map_type
|
||||
self._billing_info[service][field][mapping] = map_dict
|
||||
|
||||
def process_service(self, name, data):
|
||||
if name not in self._billing_info:
|
||||
return
|
||||
serv_b_info = self._billing_info[name]
|
||||
for entry in data:
|
||||
flat = 0
|
||||
rate = 1
|
||||
entry_desc = entry['desc']
|
||||
for field in serv_b_info:
|
||||
if field not in entry_desc:
|
||||
continue
|
||||
b_info = serv_b_info[field]
|
||||
key = entry_desc[field]
|
||||
|
||||
value = 0
|
||||
if key in b_info:
|
||||
value = b_info[key]['value']
|
||||
elif '_DEFAULT_' in b_info:
|
||||
value = b_info['_DEFAULT_']
|
||||
|
||||
if value:
|
||||
if b_info[key]['type'] == 'rate':
|
||||
rate *= value
|
||||
elif b_info[key]['type'] == 'flat':
|
||||
new_flat = 0
|
||||
new_flat = value
|
||||
if new_flat > flat:
|
||||
flat = new_flat
|
||||
entry['billing'] = {'price': flat * rate}
|
||||
|
||||
def process(self, data):
|
||||
for cur_data in data:
|
||||
cur_usage = cur_data['usage']
|
||||
for service in cur_usage:
|
||||
self.process_service(service, cur_usage[service])
|
||||
return data
|
|
@ -0,0 +1,219 @@
|
|||
# -*- 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.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, service):
|
||||
super(NoSuchService, self).__init__(
|
||||
"No such service: %s" % service)
|
||||
self.service = service
|
||||
|
||||
|
||||
class NoSuchField(Exception):
|
||||
"""Raised when the field doesn't exist for the service."""
|
||||
|
||||
def __init__(self, service, field):
|
||||
super(NoSuchField, self).__init__(
|
||||
"No such field for %s service: %s" % (service, field,))
|
||||
self.service = service
|
||||
self.field = field
|
||||
|
||||
|
||||
class NoSuchMapping(Exception):
|
||||
"""Raised when the mapping doesn't exist."""
|
||||
|
||||
def __init__(self, service, field, key):
|
||||
super(NoSuchMapping, self).__init__(
|
||||
"No such key for %s service and %s field: %s"
|
||||
% (service, field, key,))
|
||||
self.service = service
|
||||
self.field = field
|
||||
self.key = key
|
||||
|
||||
|
||||
class ServiceAlreadyExists(Exception):
|
||||
"""Raised when the service already exists."""
|
||||
|
||||
def __init__(self, service):
|
||||
super(ServiceAlreadyExists, self).__init__(
|
||||
"Service %s already exists" % service)
|
||||
self.service = service
|
||||
|
||||
|
||||
class FieldAlreadyExists(Exception):
|
||||
"""Raised when the field already exists."""
|
||||
|
||||
def __init__(self, field):
|
||||
super(FieldAlreadyExists, self).__init__(
|
||||
"Field %s already exists" % field)
|
||||
self.field = field
|
||||
|
||||
|
||||
class MappingAlreadyExists(Exception):
|
||||
"""Raised when the mapping already exists."""
|
||||
|
||||
def __init__(self, mapping):
|
||||
super(MappingAlreadyExists, self).__init__(
|
||||
"Mapping %s already exists" % mapping)
|
||||
self.mapping = mapping
|
||||
|
||||
|
||||
@six.add_metaclass(abc.ABCMeta)
|
||||
class HashMap(object):
|
||||
"""Base class for hashmap configuration."""
|
||||
|
||||
@abc.abstractmethod
|
||||
def get_migrate(self):
|
||||
"""Return a migrate manager.
|
||||
|
||||
"""
|
||||
|
||||
@abc.abstractmethod
|
||||
def get_service(self, service):
|
||||
"""Return a service object.
|
||||
|
||||
:param service: The service to filter on.
|
||||
"""
|
||||
|
||||
@abc.abstractmethod
|
||||
def get_field(self, service, field):
|
||||
"""Return a field object.
|
||||
|
||||
:param service: The service to filter on.
|
||||
:param field: The field to filter on.
|
||||
"""
|
||||
|
||||
@abc.abstractmethod
|
||||
def get_mapping(self, service, field, key):
|
||||
"""Return a field object.
|
||||
|
||||
:param service: The service to filter on.
|
||||
:param field: The field to filter on.
|
||||
:param key: The field to filter on.
|
||||
:param key: Value of the field to filter on.
|
||||
"""
|
||||
|
||||
@abc.abstractmethod
|
||||
def list_services(self):
|
||||
"""Return a list of every services.
|
||||
|
||||
"""
|
||||
|
||||
@abc.abstractmethod
|
||||
def list_fields(self, service):
|
||||
"""Return a list of every fields in a service.
|
||||
|
||||
:param service: The service to filter on.
|
||||
"""
|
||||
|
||||
@abc.abstractmethod
|
||||
def list_mappings(self, service, field):
|
||||
"""Return a list of every mapping.
|
||||
|
||||
:param service: The service to filter on.
|
||||
:param field: The key to filter on.
|
||||
"""
|
||||
|
||||
@abc.abstractmethod
|
||||
def create_service(self, service):
|
||||
"""Create a new service.
|
||||
|
||||
:param service:
|
||||
"""
|
||||
|
||||
@abc.abstractmethod
|
||||
def create_field(self, service, field):
|
||||
"""Create a new field.
|
||||
|
||||
:param service:
|
||||
:param field:
|
||||
"""
|
||||
|
||||
@abc.abstractmethod
|
||||
def create_mapping(self, service, field, key, value, map_type='rate'):
|
||||
"""Create a new service/field mapping.
|
||||
|
||||
:param service: Service the mapping is applying to.
|
||||
:param field: Field the mapping is applying to.
|
||||
:param key: Value of the field this mapping is applying to.
|
||||
:param value: Pricing value to apply to this mapping.
|
||||
:param map_type: The type of pricing rule.
|
||||
"""
|
||||
|
||||
@abc.abstractmethod
|
||||
def update_mapping(self, service, field, key, **kwargs):
|
||||
"""Update a mapping.
|
||||
|
||||
:param service: Service the mapping is applying to.
|
||||
:param field: Field the mapping is applying to.
|
||||
:param key: Value of the field this mapping is applying to.
|
||||
:param value: Pricing value to apply to this mapping.
|
||||
:param map_type: The type of pricing rule.
|
||||
"""
|
||||
|
||||
@abc.abstractmethod
|
||||
def update_or_create_mapping(self, service, field, key, **kwargs):
|
||||
"""Update or create a mapping.
|
||||
|
||||
:param service: Service the mapping is applying to.
|
||||
:param field: Field the mapping is applying to.
|
||||
:param key: Value of the field this mapping is applying to.
|
||||
:param value: Pricing value to apply to this mapping.
|
||||
:param map_type: The type of pricing rule.
|
||||
"""
|
||||
|
||||
@abc.abstractmethod
|
||||
def delete_service(self, service):
|
||||
"""Delete a service recursively.
|
||||
|
||||
:param service: Service to delete.
|
||||
"""
|
||||
|
||||
@abc.abstractmethod
|
||||
def delete_field(self, service, field):
|
||||
"""Delete a field recursively.
|
||||
|
||||
:param service: Service the field is applying to.
|
||||
:param field: field to delete.
|
||||
"""
|
||||
|
||||
@abc.abstractmethod
|
||||
def delete_mapping(self, service, field, key):
|
||||
"""Delete a mapping recursively.
|
||||
|
||||
:param service: Service the field is applying to.
|
||||
:param field: Field the mapping is applying to.
|
||||
:param key: key to delete.
|
||||
"""
|
|
@ -0,0 +1,25 @@
|
|||
# -*- 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.billing.hash.db.sqlalchemy import models
|
||||
from cloudkitty.common.db.alembic import env # noqa
|
||||
|
||||
target_metadata = models.Base.metadata
|
||||
version_table = 'hashmap_alembic'
|
||||
|
||||
|
||||
env.run_migrations_online(target_metadata, version_table)
|
|
@ -0,0 +1,22 @@
|
|||
"""${message}
|
||||
|
||||
Revision ID: ${up_revision}
|
||||
Revises: ${down_revision}
|
||||
Create Date: ${create_date}
|
||||
|
||||
"""
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = ${repr(up_revision)}
|
||||
down_revision = ${repr(down_revision)}
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
${imports if imports else ""}
|
||||
|
||||
def upgrade():
|
||||
${upgrades if upgrades else "pass"}
|
||||
|
||||
|
||||
def downgrade():
|
||||
${downgrades if downgrades else "pass"}
|
|
@ -0,0 +1,60 @@
|
|||
"""initial migration
|
||||
|
||||
Revision ID: 48676342515a
|
||||
Revises: None
|
||||
Create Date: 2014-08-05 17:13:10.323228
|
||||
|
||||
"""
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = '48676342515a'
|
||||
down_revision = None
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
def upgrade():
|
||||
### commands auto generated by Alembic - please adjust! ###
|
||||
op.create_table('hashmap_services',
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('name', sa.String(length=255), nullable=False),
|
||||
sa.PrimaryKeyConstraint('id'),
|
||||
sa.UniqueConstraint('name'),
|
||||
mysql_charset='utf8',
|
||||
mysql_engine='InnoDB'
|
||||
)
|
||||
op.create_table('hashmap_fields',
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('name', sa.String(length=255), nullable=False),
|
||||
sa.Column('service_id', sa.Integer(), nullable=False),
|
||||
sa.ForeignKeyConstraint(['service_id'], ['hashmap_services.id'], ondelete='CASCADE'),
|
||||
sa.PrimaryKeyConstraint('id'),
|
||||
sa.UniqueConstraint('service_id', 'name', name='uniq_map_service_field'),
|
||||
mysql_charset='utf8',
|
||||
mysql_engine='InnoDB'
|
||||
)
|
||||
op.create_table('hashmap_maps',
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('key', sa.String(length=255), nullable=False),
|
||||
sa.Column('value', sa.Float(), nullable=False),
|
||||
sa.Column('map_type', sa.Enum('flat', 'rate', name='enum_map_type'), nullable=False),
|
||||
sa.Column('field_id', sa.Integer(), nullable=False),
|
||||
sa.ForeignKeyConstraint(['field_id'], ['hashmap_fields.id'], ondelete='CASCADE'),
|
||||
sa.PrimaryKeyConstraint('id'),
|
||||
sa.UniqueConstraint('key', 'field_id', name='uniq_mapping'),
|
||||
mysql_charset='utf8',
|
||||
mysql_engine='InnoDB'
|
||||
)
|
||||
### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade():
|
||||
### commands auto generated by Alembic - please adjust! ###
|
||||
op.create_table('hashmap_alembic',
|
||||
sa.Column('version_num', sa.VARCHAR(length=32), nullable=False)
|
||||
)
|
||||
op.drop_table('hashmap_maps')
|
||||
op.drop_table('hashmap_fields')
|
||||
op.drop_table('hashmap_services')
|
||||
### end Alembic commands ###
|
|
@ -0,0 +1,227 @@
|
|||
# -*- 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
|
||||
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_migrate(self):
|
||||
return migration
|
||||
|
||||
def get_service(self, service):
|
||||
session = db.get_session()
|
||||
try:
|
||||
q = session.query(models.HashMapService)
|
||||
res = q.filter_by(
|
||||
name=service,
|
||||
).one()
|
||||
return res
|
||||
except sqlalchemy.orm.exc.NoResultFound:
|
||||
raise api.NoSuchService(service)
|
||||
|
||||
def get_field(self, service, field):
|
||||
session = db.get_session()
|
||||
try:
|
||||
service_db = self.get_service(service)
|
||||
q = session.query(models.HashMapField)
|
||||
res = q.filter_by(
|
||||
service_id=service_db.id,
|
||||
name=field
|
||||
).one()
|
||||
return res
|
||||
except sqlalchemy.orm.exc.NoResultFound:
|
||||
raise api.NoSuchField(service, field)
|
||||
|
||||
def get_mapping(self, service, field, key):
|
||||
session = db.get_session()
|
||||
try:
|
||||
field_db = self.get_field(service, field)
|
||||
q = session.query(models.HashMapMapping)
|
||||
res = q.filter_by(
|
||||
key=key,
|
||||
field_id=field_db.id
|
||||
).one()
|
||||
return res
|
||||
except sqlalchemy.orm.exc.NoResultFound:
|
||||
raise api.NoSuchMapping(service, field, key)
|
||||
|
||||
def list_services(self):
|
||||
session = db.get_session()
|
||||
q = session.query(models.HashMapService)
|
||||
res = q.values(
|
||||
models.HashMapService.name
|
||||
)
|
||||
return res
|
||||
|
||||
def list_fields(self, service):
|
||||
session = db.get_session()
|
||||
service_db = self.get_service(service)
|
||||
q = session.query(models.HashMapField)
|
||||
res = q.filter_by(
|
||||
service_id=service_db.id
|
||||
).values(
|
||||
models.HashMapField.name
|
||||
)
|
||||
return res
|
||||
|
||||
def list_mappings(self, service, field):
|
||||
session = db.get_session()
|
||||
field_db = self.get_field(service, field)
|
||||
q = session.query(models.HashMapMapping)
|
||||
res = q.filter_by(
|
||||
field_id=field_db.id
|
||||
).values(
|
||||
models.HashMapMapping.key
|
||||
)
|
||||
return res
|
||||
|
||||
def create_service(self, service):
|
||||
session = db.get_session()
|
||||
try:
|
||||
with session.begin():
|
||||
service_db = models.HashMapService(name=service)
|
||||
session.add(service_db)
|
||||
session.flush()
|
||||
# TODO(sheeprine): return object
|
||||
return service_db
|
||||
except exception.DBDuplicateEntry:
|
||||
raise api.ServiceAlreadyExists(service)
|
||||
|
||||
def create_field(self, service, field):
|
||||
try:
|
||||
service_db = self.get_service(service)
|
||||
except api.NoSuchService:
|
||||
service_db = self.create_service(service)
|
||||
session = db.get_session()
|
||||
try:
|
||||
with session.begin():
|
||||
field_db = models.HashMapField(
|
||||
service_id=service_db.id,
|
||||
name=field)
|
||||
session.add(field_db)
|
||||
session.flush()
|
||||
# TODO(sheeprine): return object
|
||||
return field_db
|
||||
except exception.DBDuplicateEntry:
|
||||
raise api.FieldAlreadyExists(field)
|
||||
|
||||
def create_mapping(self, service, field, key, value, map_type='rate'):
|
||||
try:
|
||||
field_db = self.get_field(service, field)
|
||||
except (api.NoSuchField, api.NoSuchService):
|
||||
field_db = self.create_field(service, field)
|
||||
session = db.get_session()
|
||||
try:
|
||||
with session.begin():
|
||||
field_map = models.HashMapMapping(
|
||||
field_id=field_db.id,
|
||||
key=key,
|
||||
value=value,
|
||||
map_type=map_type)
|
||||
session.add(field_map)
|
||||
# TODO(sheeprine): return object
|
||||
return field_map
|
||||
except exception.DBDuplicateEntry:
|
||||
raise api.MappingAlreadyExists(key)
|
||||
|
||||
def update_mapping(self, service, field, key, **kwargs):
|
||||
field_db = self.get_field(service, field)
|
||||
session = db.get_session()
|
||||
try:
|
||||
with session.begin():
|
||||
q = session.query(models.HashMapMapping)
|
||||
field_map = q.filter_by(
|
||||
key=key,
|
||||
field_id=field_db.id
|
||||
).with_lockmode('update').one()
|
||||
if kwargs:
|
||||
for attribute, value in six.iteritems(kwargs):
|
||||
if hasattr(field_map, attribute):
|
||||
setattr(field_map, attribute, value)
|
||||
else:
|
||||
raise ValueError('No such attribute: {}'.format(
|
||||
attribute))
|
||||
else:
|
||||
raise ValueError('No attribute to update.')
|
||||
return field_map
|
||||
except sqlalchemy.orm.exc.NoResultFound:
|
||||
raise api.NoSuchMapping(service, field, key)
|
||||
|
||||
def update_or_create_mapping(self, service, field, key, **kwargs):
|
||||
try:
|
||||
return self.create_mapping(
|
||||
service,
|
||||
field,
|
||||
key,
|
||||
**kwargs
|
||||
)
|
||||
except api.MappingAlreadyExists:
|
||||
return self.update_mapping(service, field, key, **kwargs)
|
||||
|
||||
def delete_service(self, service):
|
||||
session = db.get_session()
|
||||
r = utils.model_query(
|
||||
models.HashMapService,
|
||||
session
|
||||
).filter_by(
|
||||
name=service,
|
||||
).delete()
|
||||
if not r:
|
||||
raise api.NoSuchService(service)
|
||||
|
||||
def delete_field(self, service, field):
|
||||
session = db.get_session()
|
||||
service_db = self.get_service(service)
|
||||
r = utils.model_query(
|
||||
models.HashMapField,
|
||||
session
|
||||
).filter_by(
|
||||
service_id=service_db.id,
|
||||
name=field,
|
||||
).delete()
|
||||
if not r:
|
||||
raise api.NoSuchField(service, field)
|
||||
|
||||
def delete_mapping(self, service, field, key):
|
||||
session = db.get_session()
|
||||
field = self.get_field(service, field)
|
||||
r = utils.model_query(
|
||||
models.HashMapMapping,
|
||||
session
|
||||
).filter_by(
|
||||
field_id=field.id,
|
||||
key=key,
|
||||
).delete()
|
||||
if not r:
|
||||
raise api.NoSuchMapping(service, field, key)
|
|
@ -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,124 @@
|
|||
# -*- 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"}
|
||||
|
||||
|
||||
class HashMapService(Base, HashMapBase):
|
||||
"""An hashmap service.
|
||||
|
||||
"""
|
||||
|
||||
__tablename__ = 'hashmap_services'
|
||||
|
||||
id = sqlalchemy.Column(sqlalchemy.Integer,
|
||||
primary_key=True)
|
||||
name = sqlalchemy.Column(
|
||||
sqlalchemy.String(255),
|
||||
nullable=False,
|
||||
unique=True
|
||||
)
|
||||
fields = orm.relationship('HashMapField')
|
||||
|
||||
def __repr__(self):
|
||||
return ('<HashMapService[{id}]: '
|
||||
'service={service}>').format(
|
||||
id=self.id,
|
||||
service=self.name)
|
||||
|
||||
|
||||
class HashMapField(Base, HashMapBase):
|
||||
"""An hashmap field.
|
||||
|
||||
"""
|
||||
|
||||
__tablename__ = 'hashmap_fields'
|
||||
|
||||
@declarative.declared_attr
|
||||
def __table_args__(cls):
|
||||
args = (schema.UniqueConstraint('service_id', 'name',
|
||||
name='uniq_map_service_field'),
|
||||
HashMapBase.__table_args__,)
|
||||
return args
|
||||
|
||||
id = sqlalchemy.Column(sqlalchemy.Integer,
|
||||
primary_key=True)
|
||||
name = sqlalchemy.Column(sqlalchemy.String(255),
|
||||
nullable=False)
|
||||
service_id = sqlalchemy.Column(
|
||||
sqlalchemy.Integer,
|
||||
sqlalchemy.ForeignKey('hashmap_services.id',
|
||||
ondelete='CASCADE'),
|
||||
nullable=False
|
||||
)
|
||||
field_maps = orm.relationship('HashMapMapping')
|
||||
|
||||
def __repr__(self):
|
||||
return ('<HashMapField[{id}]: '
|
||||
'field={field}>').format(
|
||||
id=self.id,
|
||||
field=self.field)
|
||||
|
||||
|
||||
class HashMapMapping(Base, HashMapBase):
|
||||
"""A mapping between a field a value and a type.
|
||||
|
||||
"""
|
||||
|
||||
__tablename__ = 'hashmap_maps'
|
||||
|
||||
@declarative.declared_attr
|
||||
def __table_args__(cls):
|
||||
args = (schema.UniqueConstraint('key', 'field_id',
|
||||
name='uniq_mapping'),
|
||||
HashMapBase.__table_args__,)
|
||||
return args
|
||||
|
||||
id = sqlalchemy.Column(sqlalchemy.Integer,
|
||||
primary_key=True)
|
||||
key = sqlalchemy.Column(sqlalchemy.String(255),
|
||||
nullable=False)
|
||||
value = sqlalchemy.Column(sqlalchemy.Float,
|
||||
nullable=False)
|
||||
map_type = sqlalchemy.Column(sqlalchemy.Enum('flat',
|
||||
'rate',
|
||||
name='enum_map_type'),
|
||||
nullable=False)
|
||||
field_id = sqlalchemy.Column(sqlalchemy.Integer,
|
||||
sqlalchemy.ForeignKey('hashmap_fields.id',
|
||||
ondelete='CASCADE'),
|
||||
nullable=False)
|
||||
|
||||
def __repr__(self):
|
||||
return ('<HashMapMapping[{id}]: '
|
||||
'type={map_type} {key}={value}>').format(
|
||||
id=self.id,
|
||||
map_type=self.map_type,
|
||||
key=self.key,
|
||||
value=self.value)
|
Loading…
Reference in New Issue