Add Tables for Stateless Architecture

In stateless architecture, we have our own nova and cinder api
gateway, which require tables to store resource models. Neutron
doesn't need this because we use its original tables. A resource
routing table is also added to map resources from top to bottom.
Besides, database access functions are moved from "models" module
to a new "api" module.

Change-Id: Ib4577fa494c232302dbbd6681b7d84b9a9cef00d
This commit is contained in:
zhiyuan_cai 2015-12-07 09:46:47 +08:00
parent 16015ac91a
commit fe14b6f244
11 changed files with 627 additions and 74 deletions

View File

@ -28,7 +28,7 @@ def main(argv=None, config_files=None):
project='tricircle',
default_config_files=config_files)
migration_helpers.find_migrate_repo()
migration_helpers.sync_repo(1)
migration_helpers.sync_repo(2)
if __name__ == '__main__':

View File

@ -87,6 +87,8 @@ if [[ "$Q_ENABLE_TRICIRCLE" == "True" ]]; then
elif [[ "$1" == "stack" && "$2" == "post-config" ]]; then
echo_summary "Configuring Tricircle"
sudo install -d -o $STACK_USER -m 755 $TRICIRCLE_CONF_DIR
configure_tricircle_api
echo export PYTHONPATH=\$PYTHONPATH:$TRICIRCLE_DIR >> $RC_DIR/.localrc.auto

View File

@ -22,6 +22,7 @@ retrying!=1.3.0,>=1.2.3 # Apache-2.0
SQLAlchemy<1.1.0,>=0.9.9
WebOb>=1.2.3
python-cinderclient>=1.3.1
python-glanceclient>=0.18.0
python-keystoneclient!=1.8.0,>=1.6.0
python-neutronclient>=2.6.0
python-novaclient>=2.29.0,!=2.33.0

View File

@ -24,7 +24,7 @@ from tricircle.common import client
import tricircle.common.context as t_context
from tricircle.common import exceptions
from tricircle.common import utils
from tricircle.db import models
import tricircle.db.api as db_api
LOG = logging.getLogger(__name__)
@ -126,14 +126,14 @@ class SitesController(rest.RestController):
def get_one(self, site_id):
context = _extract_context_from_environ(_get_environment())
try:
return {'site': models.get_site(context, site_id)}
return {'site': db_api.get_site(context, site_id)}
except exceptions.ResourceNotFound:
pecan.abort(404, 'Site with id %s not found' % site_id)
@expose()
def get_all(self):
context = _extract_context_from_environ(_get_environment())
sites = models.list_sites(context, [])
sites = db_api.list_sites(context, [])
return {'sites': sites}
@expose()
@ -152,7 +152,7 @@ class SitesController(rest.RestController):
site_filters = [{'key': 'site_name', 'comparator': 'eq',
'value': site_name}]
sites = models.list_sites(context, site_filters)
sites = db_api.list_sites(context, site_filters)
if sites:
pecan.abort(409, 'Site with name %s exists' % site_name)
return
@ -165,7 +165,7 @@ class SitesController(rest.RestController):
site_dict = {'site_id': str(uuid.uuid4()),
'site_name': site_name,
'az_id': az_name}
site = models.create_site(context, site_dict)
site = db_api.create_site(context, site_dict)
except Exception as e:
LOG.debug(e.message)
pecan.abort(500, 'Fail to create site')
@ -182,7 +182,7 @@ class SitesController(rest.RestController):
except Exception as e:
LOG.debug(e.message)
# delete previously created site
models.delete_site(context, site['site_id'])
db_api.delete_site(context, site['site_id'])
pecan.abort(500, 'Fail to create aggregate')
return
pecan.response.status = 201

View File

@ -29,6 +29,7 @@ from oslo_log import log as logging
import tricircle.common.context as tricircle_context
from tricircle.common import exceptions
from tricircle.common import resource_handle
from tricircle.db import api
from tricircle.db import models
@ -165,7 +166,7 @@ class Client(object):
return region_service_endpoint_map
def _get_config_with_retry(self, cxt, filters, site, service, retry):
conf_list = models.list_site_service_configurations(cxt, filters)
conf_list = api.list_site_service_configurations(cxt, filters)
if len(conf_list) > 1:
raise exceptions.EndpointNotUnique(site, service)
if len(conf_list) == 0:
@ -182,7 +183,7 @@ class Client(object):
site_filters = [{'key': 'site_name',
'comparator': 'eq',
'value': self.site_name}]
site_list = models.list_sites(cxt, site_filters)
site_list = api.list_sites(cxt, site_filters)
if len(site_list) == 0:
raise exceptions.ResourceNotFound(models.Site,
self.site_name)
@ -218,7 +219,7 @@ class Client(object):
# use region name to query site
site_filters = [{'key': 'site_name', 'comparator': 'eq',
'value': region}]
site_list = models.list_sites(cxt, site_filters)
site_list = api.list_sites(cxt, site_filters)
# skip region/site not registered in cascade service
if len(site_list) != 1:
continue
@ -228,7 +229,7 @@ class Client(object):
'value': site_id},
{'key': 'service_type', 'comparator': 'eq',
'value': service}]
config_list = models.list_site_service_configurations(
config_list = api.list_site_service_configurations(
cxt, config_filters)
if len(config_list) > 1:
@ -237,7 +238,7 @@ class Client(object):
config_id = config_list[0]['service_id']
update_dict = {
'service_url': endpoint_map[region][service]}
models.update_site_service_configuration(
api.update_site_service_configuration(
cxt, config_id, update_dict)
else:
config_dict = {
@ -246,7 +247,7 @@ class Client(object):
'service_type': service,
'service_url': endpoint_map[region][service]
}
models.create_site_service_configuration(
api.create_site_service_configuration(
cxt, config_dict)
def get_endpoint(self, cxt, site_id, service):

73
tricircle/db/api.py Normal file
View File

@ -0,0 +1,73 @@
# Copyright 2015 Huawei Technologies Co., Ltd.
# All Rights Reserved
#
# 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.
from tricircle.db import core
from tricircle.db import models
def create_site(context, site_dict):
with context.session.begin():
return core.create_resource(context, models.Site, site_dict)
def delete_site(context, site_id):
with context.session.begin():
return core.delete_resource(context, models.Site, site_id)
def get_site(context, site_id):
with context.session.begin():
return core.get_resource(context, models.Site, site_id)
def list_sites(context, filters):
with context.session.begin():
return core.query_resource(context, models.Site, filters)
def update_site(context, site_id, update_dict):
with context.session.begin():
return core.update_resource(context, models.Site, site_id, update_dict)
def create_site_service_configuration(context, config_dict):
with context.session.begin():
return core.create_resource(context, models.SiteServiceConfiguration,
config_dict)
def delete_site_service_configuration(context, config_id):
with context.session.begin():
return core.delete_resource(context, models.SiteServiceConfiguration,
config_id)
def get_site_service_configuration(context, config_id):
with context.session.begin():
return core.get_resource(context, models.SiteServiceConfiguration,
config_id)
def list_site_service_configurations(context, filters):
with context.session.begin():
return core.query_resource(context, models.SiteServiceConfiguration,
filters)
def update_site_service_configuration(context, config_id, update_dict):
with context.session.begin():
return core.update_resource(
context, models.SiteServiceConfiguration, config_id, update_dict)

View File

@ -0,0 +1,187 @@
# Copyright 2015 Huawei Technologies Co., Ltd.
# All Rights Reserved
#
# 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.
import migrate
import sqlalchemy as sql
from sqlalchemy.dialects import mysql
def MediumText():
return sql.Text().with_variant(mysql.MEDIUMTEXT(), 'mysql')
def upgrade(migrate_engine):
meta = sql.MetaData()
meta.bind = migrate_engine
aggregates = sql.Table(
'aggregates', meta,
sql.Column('id', sql.Integer, primary_key=True),
sql.Column('name', sql.String(255)),
mysql_engine='InnoDB',
mysql_charset='utf8')
aggregate_metadata = sql.Table(
'aggregate_metadata', meta,
sql.Column('id', sql.Integer, primary_key=True),
sql.Column('key', sql.String(255), nullable=False),
sql.Column('value', sql.String(255), nullable=False),
sql.Column('aggregate_id', sql.Integer, nullable=False),
mysql_engine='InnoDB',
mysql_charset='utf8')
instance_types = sql.Table(
'instance_types', meta,
sql.Column('id', sql.Integer, primary_key=True),
sql.Column('name', sql.String(255), unique=True),
sql.Column('memory_mb', sql.Integer, nullable=False),
sql.Column('vcpus', sql.Integer, nullable=False),
sql.Column('root_gb', sql.Integer),
sql.Column('ephemeral_gb', sql.Integer),
sql.Column('flavorid', sql.String(255), unique=True),
sql.Column('swap', sql.Integer, nullable=False, default=0),
sql.Column('rxtx_factor', sql.Float, default=1),
sql.Column('vcpu_weight', sql.Integer),
sql.Column('disabled', sql.Boolean, default=False),
sql.Column('is_public', sql.Boolean, default=True),
sql.Column('created_at', sql.DateTime),
sql.Column('updated_at', sql.DateTime),
mysql_engine='InnoDB',
mysql_charset='utf8')
instance_type_projects = sql.Table(
'instance_type_projects', meta,
sql.Column('id', sql.Integer, primary_key=True),
sql.Column('instance_type_id', sql.Integer, nullable=False),
sql.Column('project_id', sql.String(255)),
sql.Column('created_at', sql.DateTime),
sql.Column('updated_at', sql.DateTime),
migrate.UniqueConstraint(
'instance_type_id', 'project_id',
name='uniq_instance_type_projects0instance_type_id0project_id'),
mysql_engine='InnoDB',
mysql_charset='utf8')
instance_type_extra_specs = sql.Table(
'instance_type_extra_specs', meta,
sql.Column('id', sql.Integer, primary_key=True),
sql.Column('key', sql.String(255)),
sql.Column('value', sql.String(255)),
sql.Column('instance_type_id', sql.Integer, nullable=False),
sql.Column('created_at', sql.DateTime),
sql.Column('updated_at', sql.DateTime),
migrate.UniqueConstraint(
'instance_type_id', 'key',
name='uniq_instance_type_extra_specs0instance_type_id0key'),
mysql_engine='InnoDB',
mysql_charset='utf8')
enum = sql.Enum('ssh', 'x509', metadata=meta, name='keypair_types')
enum.create()
key_pairs = sql.Table(
'key_pairs', meta,
sql.Column('id', sql.Integer, primary_key=True, nullable=False),
sql.Column('name', sql.String(255), nullable=False),
sql.Column('user_id', sql.String(255)),
sql.Column('fingerprint', sql.String(255)),
sql.Column('public_key', MediumText()),
sql.Column('type', enum, nullable=False, server_default='ssh'),
sql.Column('created_at', sql.DateTime),
sql.Column('updated_at', sql.DateTime),
migrate.UniqueConstraint(
'user_id', 'name',
name='uniq_key_pairs0user_id0name'),
mysql_engine='InnoDB',
mysql_charset='utf8')
quotas = sql.Table(
'quotas', meta,
sql.Column('id', sql.Integer, primary_key=True),
sql.Column('project_id', sql.String(255)),
sql.Column('resource', sql.String(255), nullable=False),
sql.Column('hard_limit', sql.Integer),
sql.Column('created_at', sql.DateTime),
sql.Column('updated_at', sql.DateTime),
migrate.UniqueConstraint(
'project_id', 'resource',
name='uniq_quotas0project_id0resource'),
mysql_engine='InnoDB',
mysql_charset='utf8')
volume_types = sql.Table(
'volume_types', meta,
sql.Column('id', sql.String(36), primary_key=True),
sql.Column('name', sql.String(255)),
sql.Column('description', sql.String(255)),
sql.Column('qos_specs_id', sql.String(36)),
sql.Column('is_public', sql.Boolean, default=True),
sql.Column('created_at', sql.DateTime),
sql.Column('updated_at', sql.DateTime),
mysql_engine='InnoDB',
mysql_charset='utf8')
quality_of_service_specs = sql.Table(
'quality_of_service_specs', meta,
sql.Column('id', sql.String(36), primary_key=True),
sql.Column('specs_id', sql.String(36)),
sql.Column('key', sql.String(255)),
sql.Column('value', sql.String(255)),
sql.Column('created_at', sql.DateTime),
sql.Column('updated_at', sql.DateTime),
mysql_engine='InnoDB',
mysql_charset='utf8')
cascaded_sites_resource_routing = sql.Table(
'cascaded_sites_resource_routing', meta,
sql.Column('id', sql.Integer, primary_key=True),
sql.Column('top_id', sql.String(length=36), nullable=False),
sql.Column('bottom_id', sql.String(length=36)),
sql.Column('site_id', sql.String(length=64), nullable=False),
sql.Column('created_at', sql.DateTime),
sql.Column('updated_at', sql.DateTime),
mysql_engine='InnoDB',
mysql_charset='utf8')
tables = [aggregates, aggregate_metadata, instance_types,
instance_type_projects, instance_type_extra_specs, key_pairs,
quotas, volume_types, quality_of_service_specs,
cascaded_sites_resource_routing]
for table in tables:
table.create()
cascaded_sites = sql.Table('cascaded_sites', meta, autoload=True)
fkeys = [{'columns': [instance_type_projects.c.instance_type_id],
'references': [instance_types.c.id]},
{'columns': [instance_type_extra_specs.c.instance_type_id],
'references': [instance_types.c.id]},
{'columns': [volume_types.c.qos_specs_id],
'references': [quality_of_service_specs.c.id]},
{'columns': [quality_of_service_specs.c.specs_id],
'references': [quality_of_service_specs.c.id]},
{'columns': [aggregate_metadata.c.aggregate_id],
'references': [aggregates.c.id]},
{'columns': [cascaded_sites_resource_routing.c.site_id],
'references': [cascaded_sites.c.site_id]}]
for fkey in fkeys:
migrate.ForeignKeyConstraint(columns=fkey['columns'],
refcolumns=fkey['references'],
name=fkey.get('name')).create()
def downgrade(migrate_engine):
raise NotImplementedError('downgrade not support')

View File

@ -14,62 +14,221 @@
# under the License.
from oslo_db.sqlalchemy import models
import sqlalchemy as sql
from sqlalchemy.dialects import mysql
from sqlalchemy import schema
from tricircle.db import core
def create_site(context, site_dict):
with context.session.begin():
return core.create_resource(context, Site, site_dict)
def MediumText():
return sql.Text().with_variant(mysql.MEDIUMTEXT(), 'mysql')
def delete_site(context, site_id):
with context.session.begin():
return core.delete_resource(context, Site, site_id)
# Resource Model
class Aggregate(core.ModelBase, core.DictBase, models.TimestampMixin):
"""Represents a cluster of hosts that exists in this zone."""
__tablename__ = 'aggregates'
attributes = ['id', 'name', 'created_at', 'updated_at']
id = sql.Column(sql.Integer, primary_key=True)
name = sql.Column(sql.String(255))
def get_site(context, site_id):
with context.session.begin():
return core.get_resource(context, Site, site_id)
class AggregateMetadata(core.ModelBase, core.DictBase, models.TimestampMixin):
"""Represents a metadata key/value pair for an aggregate."""
__tablename__ = 'aggregate_metadata'
__table_args__ = (
sql.Index('aggregate_metadata_key_idx', 'key'),
schema.UniqueConstraint(
'aggregate_id', 'key',
name='uniq_aggregate_metadata0aggregate_id0key'),
)
attributes = ['id', 'key', 'value', 'aggregate_id',
'created_at', 'updated_at']
id = sql.Column(sql.Integer, primary_key=True)
key = sql.Column(sql.String(255), nullable=False)
value = sql.Column(sql.String(255), nullable=False)
aggregate_id = sql.Column(sql.Integer,
sql.ForeignKey('aggregates.id'), nullable=False)
def list_sites(context, filters):
with context.session.begin():
return core.query_resource(context, Site, filters)
class InstanceTypes(core.ModelBase, core.DictBase, models.TimestampMixin):
"""Represents possible flavors for instances.
Note: instance_type and flavor are synonyms and the term instance_type is
deprecated and in the process of being removed.
"""
__tablename__ = 'instance_types'
attributes = ['id', 'name', 'memory_mb', 'vcpus', 'root_gb',
'ephemeral_gb', 'flavorid', 'swap', 'rxtx_factor',
'vcpu_weight', 'disabled', 'is_public', 'created_at',
'updated_at']
# Internal only primary key/id
id = sql.Column(sql.Integer, primary_key=True)
name = sql.Column(sql.String(255), unique=True)
memory_mb = sql.Column(sql.Integer, nullable=False)
vcpus = sql.Column(sql.Integer, nullable=False)
root_gb = sql.Column(sql.Integer)
ephemeral_gb = sql.Column(sql.Integer)
# Public facing id will be renamed public_id
flavorid = sql.Column(sql.String(255), unique=True)
swap = sql.Column(sql.Integer, nullable=False, default=0)
rxtx_factor = sql.Column(sql.Float, default=1)
vcpu_weight = sql.Column(sql.Integer)
disabled = sql.Column(sql.Boolean, default=False)
is_public = sql.Column(sql.Boolean, default=True)
def update_site(context, site_id, update_dict):
with context.session.begin():
return core.update_resource(context, Site, site_id, update_dict)
class InstanceTypeProjects(core.ModelBase, core.DictBase,
models.TimestampMixin):
"""Represent projects associated instance_types."""
__tablename__ = 'instance_type_projects'
__table_args__ = (schema.UniqueConstraint(
'instance_type_id', 'project_id',
name='uniq_instance_type_projects0instance_type_id0project_id'),
)
attributes = ['id', 'instance_type_id', 'project_id', 'created_at',
'updated_at']
id = sql.Column(sql.Integer, primary_key=True)
instance_type_id = sql.Column(sql.Integer,
sql.ForeignKey('instance_types.id'),
nullable=False)
project_id = sql.Column(sql.String(255))
def create_site_service_configuration(context, config_dict):
with context.session.begin():
return core.create_resource(context, SiteServiceConfiguration,
config_dict)
class InstanceTypeExtraSpecs(core.ModelBase, core.DictBase,
models.TimestampMixin):
"""Represents additional specs as key/value pairs for an instance_type."""
__tablename__ = 'instance_type_extra_specs'
__table_args__ = (
sql.Index('instance_type_extra_specs_instance_type_id_key_idx',
'instance_type_id', 'key'),
schema.UniqueConstraint(
'instance_type_id', 'key',
name='uniq_instance_type_extra_specs0instance_type_id0key'),
{'mysql_collate': 'utf8_bin'},
)
attributes = ['id', 'key', 'value', 'instance_type_id', 'created_at',
'updated_at']
id = sql.Column(sql.Integer, primary_key=True)
key = sql.Column(sql.String(255))
value = sql.Column(sql.String(255))
instance_type_id = sql.Column(sql.Integer,
sql.ForeignKey('instance_types.id'),
nullable=False)
def delete_site_service_configuration(context, config_id):
with context.session.begin():
return core.delete_resource(context,
SiteServiceConfiguration, config_id)
class KeyPair(core.ModelBase, core.DictBase, models.TimestampMixin):
"""Represents a public key pair for ssh / WinRM."""
__tablename__ = 'key_pairs'
__table_args__ = (
schema.UniqueConstraint('user_id', 'name',
name='uniq_key_pairs0user_id0name'),
)
attributes = ['id', 'name', 'user_id', 'fingerprint', 'public_key', 'type',
'created_at', 'updated_at']
id = sql.Column(sql.Integer, primary_key=True, nullable=False)
name = sql.Column(sql.String(255), nullable=False)
user_id = sql.Column(sql.String(255))
fingerprint = sql.Column(sql.String(255))
public_key = sql.Column(MediumText())
type = sql.Column(sql.Enum('ssh', 'x509', name='keypair_types'),
nullable=False, server_default='ssh')
def list_site_service_configurations(context, filters):
with context.session.begin():
return core.query_resource(context, SiteServiceConfiguration, filters)
class Quota(core.ModelBase, core.DictBase, models.TimestampMixin):
"""Represents a single quota override for a project.
If there is no row for a given project id and resource, then the
default for the quota class is used. If there is no row for a
given quota class and resource, then the default for the
deployment is used. If the row is present but the hard limit is
Null, then the resource is unlimited.
"""
__tablename__ = 'quotas'
__table_args__ = (
schema.UniqueConstraint('project_id', 'resource',
name='uniq_quotas0project_id0resource'),
)
attributes = ['id', 'project_id', 'resource', 'hard_limit',
'created_at', 'updated_at']
id = sql.Column(sql.Integer, primary_key=True)
project_id = sql.Column(sql.String(255))
resource = sql.Column(sql.String(255), nullable=False)
hard_limit = sql.Column(sql.Integer)
def update_site_service_configuration(context, config_id, update_dict):
with context.session.begin():
return core.update_resource(
context, SiteServiceConfiguration, config_id, update_dict)
class VolumeTypes(core.ModelBase, core.DictBase, models.TimestampMixin):
"""Represent possible volume_types of volumes offered."""
__tablename__ = "volume_types"
attributes = ['id', 'name', 'description', 'qos_specs_id', 'is_public',
'created_at', 'updated_at']
id = sql.Column(sql.String(36), primary_key=True)
name = sql.Column(sql.String(255))
description = sql.Column(sql.String(255))
# A reference to qos_specs entity
qos_specs_id = sql.Column(sql.String(36),
sql.ForeignKey('quality_of_service_specs.id'))
is_public = sql.Column(sql.Boolean, default=True)
class QualityOfServiceSpecs(core.ModelBase, core.DictBase,
models.TimestampMixin):
"""Represents QoS specs as key/value pairs.
QoS specs is standalone entity that can be associated/disassociated
with volume types (one to many relation). Adjacency list relationship
pattern is used in this model in order to represent following hierarchical
data with in flat table, e.g, following structure
qos-specs-1 'Rate-Limit'
|
+------> consumer = 'front-end'
+------> total_bytes_sec = 1048576
+------> total_iops_sec = 500
qos-specs-2 'QoS_Level1'
|
+------> consumer = 'back-end'
+------> max-iops = 1000
+------> min-iops = 200
is represented by:
id specs_id key value
------ -------- ------------- -----
UUID-1 NULL QoSSpec_Name Rate-Limit
UUID-2 UUID-1 consumer front-end
UUID-3 UUID-1 total_bytes_sec 1048576
UUID-4 UUID-1 total_iops_sec 500
UUID-5 NULL QoSSpec_Name QoS_Level1
UUID-6 UUID-5 consumer back-end
UUID-7 UUID-5 max-iops 1000
UUID-8 UUID-5 min-iops 200
"""
__tablename__ = 'quality_of_service_specs'
attributes = ['id', 'specs_id', 'key', 'value', 'created_at', 'updated_at']
id = sql.Column(sql.String(36), primary_key=True)
specs_id = sql.Column(sql.String(36), sql.ForeignKey(id))
key = sql.Column(sql.String(255))
value = sql.Column(sql.String(255))
# Site Model
class Site(core.ModelBase, core.DictBase):
__tablename__ = 'cascaded_sites'
attributes = ['site_id', 'site_name', 'az_id']
site_id = sql.Column('site_id', sql.String(length=64), primary_key=True)
site_name = sql.Column('site_name', sql.String(length=64), unique=True,
nullable=False)
@ -79,6 +238,7 @@ class Site(core.ModelBase, core.DictBase):
class SiteServiceConfiguration(core.ModelBase, core.DictBase):
__tablename__ = 'cascaded_site_service_configuration'
attributes = ['service_id', 'site_id', 'service_type', 'service_url']
service_id = sql.Column('service_id', sql.String(length=64),
primary_key=True)
site_id = sql.Column('site_id', sql.String(length=64),
@ -88,3 +248,22 @@ class SiteServiceConfiguration(core.ModelBase, core.DictBase):
nullable=False)
service_url = sql.Column('service_url', sql.String(length=512),
nullable=False)
# Routing Model
class ResourceRouting(core.ModelBase, core.DictBase, models.TimestampMixin):
__tablename__ = 'cascaded_sites_resource_routing'
__table_args__ = (
schema.UniqueConstraint(
'top_id', 'site_id',
name='cascaded_sites_resource_routing0top_id0site_id'),
)
attributes = ['id', 'top_id', 'bottom_id', 'site_id',
'created_at', 'updated_at']
id = sql.Column('id', sql.Integer, primary_key=True)
top_id = sql.Column('top_id', sql.String(length=36), nullable=False)
bottom_id = sql.Column('bottom_id', sql.String(length=36))
site_id = sql.Column('site_id', sql.String(length=64),
sql.ForeignKey('cascaded_sites.site_id'),
nullable=False)

View File

@ -22,8 +22,8 @@ import pecan
import tricircle.api.controllers.root as root_controller
from tricircle.common import client
from tricircle.common import context
from tricircle.db import api
from tricircle.db import core
from tricircle.db import models
class ControllerTest(unittest.TestCase):
@ -52,7 +52,7 @@ class SitesControllerTest(ControllerTest):
def test_post_top_site(self):
kw = {'name': 'TopSite', 'top': True}
site_id = self.controller.post(**kw)['site']['site_id']
site = models.get_site(self.context, site_id)
site = api.get_site(self.context, site_id)
self.assertEqual(site['site_name'], 'TopSite')
self.assertEqual(site['az_id'], '')
@ -60,7 +60,7 @@ class SitesControllerTest(ControllerTest):
def test_post_bottom_site(self, mock_method):
kw = {'name': 'BottomSite'}
site_id = self.controller.post(**kw)['site']['site_id']
site = models.get_site(self.context, site_id)
site = api.get_site(self.context, site_id)
self.assertEqual(site['site_name'], 'BottomSite')
self.assertEqual(site['az_id'], 'az_BottomSite')
mock_method.assert_called_once_with('aggregate', self.context,
@ -100,7 +100,7 @@ class SitesControllerTest(ControllerTest):
'az_Site%d' % i) for i in xrange(2, 4)]
mock_method.assert_has_calls(calls)
@patch.object(models, 'create_site')
@patch.object(api, 'create_site')
def test_post_create_site_exception(self, mock_method):
mock_method.side_effect = Exception
kw = {'name': 'BottomSite'}
@ -118,7 +118,7 @@ class SitesControllerTest(ControllerTest):
site_filter = [{'key': 'site_name',
'comparator': 'eq',
'value': 'BottomSite'}]
sites = models.list_sites(self.context, site_filter)
sites = api.list_sites(self.context, site_filter)
self.assertEqual(len(sites), 0)
def test_get_one(self):

View File

@ -25,8 +25,8 @@ from tricircle.common import client
from tricircle.common import context
from tricircle.common import exceptions
from tricircle.common import resource_handle
from tricircle.db import api
from tricircle.db import core
from tricircle.db import models
FAKE_AZ = 'fake_az'
@ -138,8 +138,8 @@ class ClientTest(unittest.TestCase):
'service_type': FAKE_TYPE,
'service_url': FAKE_URL
}
models.create_site(self.context, site_dict)
models.create_site_service_configuration(self.context, config_dict)
api.create_site(self.context, site_dict)
api.create_site_service_configuration(self.context, config_dict)
global FAKE_RESOURCES
FAKE_RESOURCES = [{'name': 'res1'}, {'name': 'res2'}]
@ -189,7 +189,7 @@ class ClientTest(unittest.TestCase):
cfg.CONF.set_override(name='auto_refresh_endpoint', override=False,
group='client')
# delete the configuration so endpoint cannot be found
models.delete_site_service_configuration(self.context, FAKE_SERVICE_ID)
api.delete_site_service_configuration(self.context, FAKE_SERVICE_ID)
# auto refresh set to False, directly raise exception
self.assertRaises(exceptions.EndpointNotFound,
self.client.list_resources,
@ -211,7 +211,7 @@ class ClientTest(unittest.TestCase):
cfg.CONF.set_override(name='auto_refresh_endpoint', override=True,
group='client')
# delete the configuration so endpoint cannot be found
models.delete_site_service_configuration(self.context, FAKE_SERVICE_ID)
api.delete_site_service_configuration(self.context, FAKE_SERVICE_ID)
self.client._get_admin_token = mock.Mock()
self.client._get_endpoint_from_keystone = mock.Mock()
@ -231,7 +231,7 @@ class ClientTest(unittest.TestCase):
'service_type': FAKE_TYPE,
'service_url': FAKE_URL
}
models.create_site_service_configuration(self.context, config_dict)
api.create_site_service_configuration(self.context, config_dict)
self.assertRaises(exceptions.EndpointNotUnique,
self.client.list_resources,
FAKE_RESOURCE, self.context, [])
@ -241,9 +241,9 @@ class ClientTest(unittest.TestCase):
group='client')
update_dict = {'service_url': FAKE_URL_INVALID}
# update url to an invalid one
models.update_site_service_configuration(self.context,
FAKE_SERVICE_ID,
update_dict)
api.update_site_service_configuration(self.context,
FAKE_SERVICE_ID,
update_dict)
# auto refresh set to False, directly raise exception
self.assertRaises(exceptions.EndpointNotAvailable,
@ -255,9 +255,9 @@ class ClientTest(unittest.TestCase):
group='client')
update_dict = {'service_url': FAKE_URL_INVALID}
# update url to an invalid one
models.update_site_service_configuration(self.context,
FAKE_SERVICE_ID,
update_dict)
api.update_site_service_configuration(self.context,
FAKE_SERVICE_ID,
update_dict)
self.client._get_admin_token = mock.Mock()
self.client._get_endpoint_from_keystone = mock.Mock()
@ -269,8 +269,8 @@ class ClientTest(unittest.TestCase):
FAKE_RESOURCE, self.context, [])
self.assertEqual(resources, [{'name': 'res1'}, {'name': 'res2'}])
@patch.object(models, 'create_site_service_configuration')
@patch.object(models, 'update_site_service_configuration')
@patch.object(api, 'create_site_service_configuration')
@patch.object(api, 'update_site_service_configuration')
def test_update_endpoint_from_keystone(self, update_mock, create_mock):
self.client._get_admin_token = mock.Mock()
self.client._get_endpoint_from_keystone = mock.Mock()

View File

@ -14,14 +14,95 @@
# under the License.
import inspect
import unittest
import oslo_db.exception
import sqlalchemy as sql
from tricircle.common import context
from tricircle.common import exceptions
from tricircle.db import api
from tricircle.db import core
from tricircle.db import models
def _get_field_value(column):
"""Get field value for resource creating
returning None indicates that not setting this field in resource dict
"""
if column.nullable:
# just skip nullable column
return None
if isinstance(column.type, sql.Text):
return 'fake_text'
elif isinstance(column.type, sql.Enum):
return column.type.enums[0]
elif isinstance(column.type, sql.String):
return 'fake_str'
elif isinstance(column.type, sql.Integer):
return 1
elif isinstance(column.type, sql.Float):
return 1.0
elif isinstance(column.type, sql.Boolean):
return True
else:
return None
def _construct_resource_dict(resource_class):
ret_dict = {}
for field in inspect.getmembers(resource_class):
if field[0] in resource_class.attributes:
field_value = _get_field_value(field[1])
if field_value is not None:
ret_dict[field[0]] = field_value
return ret_dict
def _sort_model_by_foreign_key(resource_class_list):
"""Apply topology sorting to obey foreign key constraints"""
relation_map = {}
table_map = {}
# {table: (set(depend_on_table), set(depended_by_table))}
for resource_class in resource_class_list:
table = resource_class.__tablename__
if table not in relation_map:
relation_map[table] = (set(), set())
if table not in table_map:
table_map[table] = resource_class
for field in inspect.getmembers(resource_class):
if field[0] in resource_class.attributes:
f_keys = field[1].foreign_keys
for f_key in f_keys:
f_table = f_key.column.table.name
# just skip self reference
if table == f_table:
continue
relation_map[table][0].add(f_table)
if f_table not in relation_map:
relation_map[f_table] = (set(), set())
relation_map[f_table][1].add(table)
sorted_list = []
total = len(relation_map)
while len(sorted_list) < total:
candidate_table = None
for table in relation_map:
# no depend-on table
if not relation_map[table][0]:
candidate_table = table
sorted_list.append(candidate_table)
for _table in relation_map[table][1]:
relation_map[_table][0].remove(table)
break
del relation_map[candidate_table]
return [table_map[table] for table in sorted_list]
class ModelsTest(unittest.TestCase):
def setUp(self):
core.initialize()
@ -40,7 +121,7 @@ class ModelsTest(unittest.TestCase):
site = {'site_id': 'test_site_uuid',
'site_name': 'test_site',
'az_id': 'test_az_uuid'}
site_ret = models.create_site(self.context, site)
site_ret = api.create_site(self.context, site)
self.assertEqual(site_ret, site)
configuration = {
@ -49,19 +130,19 @@ class ModelsTest(unittest.TestCase):
'service_type': 'nova',
'service_url': 'http://test_url'
}
config_ret = models.create_site_service_configuration(self.context,
configuration)
config_ret = api.create_site_service_configuration(self.context,
configuration)
self.assertEqual(config_ret, configuration)
def test_update(self):
site = {'site_id': 'test_site_uuid',
'site_name': 'test_site',
'az_id': 'test_az1_uuid'}
models.create_site(self.context, site)
api.create_site(self.context, site)
update_dict = {'site_id': 'fake_uuid',
'site_name': 'test_site2',
'az_id': 'test_az2_uuid'}
ret = models.update_site(self.context, 'test_site_uuid', update_dict)
ret = api.update_site(self.context, 'test_site_uuid', update_dict)
# primary key value will not be updated
self.assertEqual(ret['site_id'], 'test_site_uuid')
self.assertEqual(ret['site_name'], 'test_site2')
@ -71,9 +152,9 @@ class ModelsTest(unittest.TestCase):
site = {'site_id': 'test_site_uuid',
'site_name': 'test_site',
'az_id': 'test_az_uuid'}
models.create_site(self.context, site)
models.delete_site(self.context, 'test_site_uuid')
self.assertRaises(exceptions.ResourceNotFound, models.get_site,
api.create_site(self.context, site)
api.delete_site(self.context, 'test_site_uuid')
self.assertRaises(exceptions.ResourceNotFound, api.get_site,
self.context, 'test_site_uuid')
def test_query(self):
@ -83,19 +164,48 @@ class ModelsTest(unittest.TestCase):
site2 = {'site_id': 'test_site2_uuid',
'site_name': 'test_site2',
'az_id': 'test_az2_uuid'}
models.create_site(self.context, site1)
models.create_site(self.context, site2)
api.create_site(self.context, site1)
api.create_site(self.context, site2)
filters = [{'key': 'site_name',
'comparator': 'eq',
'value': 'test_site2'}]
sites = models.list_sites(self.context, filters)
sites = api.list_sites(self.context, filters)
self.assertEqual(len(sites), 1)
self.assertEqual(sites[0], site2)
filters = [{'key': 'site_name',
'comparator': 'eq',
'value': 'test_site3'}]
sites = models.list_sites(self.context, filters)
sites = api.list_sites(self.context, filters)
self.assertEqual(len(sites), 0)
def test_resources(self):
"""Create all the resources to test model definition"""
try:
model_list = []
for _, model_class in inspect.getmembers(models):
if inspect.isclass(model_class) and (
issubclass(model_class, core.ModelBase)):
model_list.append(model_class)
for model_class in _sort_model_by_foreign_key(model_list):
create_dict = _construct_resource_dict(model_class)
with self.context.session.begin():
core.create_resource(
self.context, model_class, create_dict)
except Exception:
self.fail('test_resources raised Exception unexpectedly')
def test_resource_routing_unique_key(self):
site = {'site_id': 'test_site1_uuid',
'site_name': 'test_site1',
'az_id': 'test_az1_uuid'}
api.create_site(self.context, site)
routing = {'top_id': 'top_uuid',
'site_id': 'test_site1_uuid'}
with self.context.session.begin():
core.create_resource(self.context, models.ResourceRouting, routing)
self.assertRaises(oslo_db.exception.DBDuplicateEntry,
core.create_resource,
self.context, models.ResourceRouting, routing)
def tearDown(self):
core.ModelBase.metadata.drop_all(core.get_engine())