Implemented HashMap API

Change-Id: Id9f02088b9bbb0da730507029c8c375b8a95531d
This commit is contained in:
Stéphane Albert 2014-08-08 15:53:57 +02:00
parent 2efb97e9f0
commit 204783c2ca
11 changed files with 984 additions and 86 deletions

View File

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

View File

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

View File

View File

@ -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.
"""

View File

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

View File

@ -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"}

View File

@ -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 ###

View File

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

View File

@ -0,0 +1,47 @@
# -*- coding: utf-8 -*-
# Copyright 2014 Objectif Libre
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
#
# @author: Stéphane Albert
#
import os
from cloudkitty.common.db.alembic import migration
ALEMBIC_REPO = os.path.join(os.path.dirname(__file__), 'alembic')
def upgrade(revision):
config = migration.load_alembic_config(ALEMBIC_REPO)
return migration.upgrade(config, revision)
def downgrade(revision):
config = migration.load_alembic_config(ALEMBIC_REPO)
return migration.downgrade(config, revision)
def version():
config = migration.load_alembic_config(ALEMBIC_REPO)
return migration.version(config)
def revision(message, autogenerate):
config = migration.load_alembic_config(ALEMBIC_REPO)
return migration.revision(config, message, autogenerate)
def stamp(revision):
config = migration.load_alembic_config(ALEMBIC_REPO)
return migration.stamp(config, revision)

View File

@ -0,0 +1,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)