Fix DB migration script

Problems we had:
1. If the collation of the DB and table is utf8 and the columns we want to
   index are 512 chars, it is too big for the index since the max key length
   is 767 bytes. Following error message observed:
   (OperationalError) (1071, 'Specified key was too long; max key length is 767 bytes')

2. During migration to Alembic we missed the unique field for the
   'fully_qualified_name' column of the table 'package'. This change
   fixes that issue.

Fixes:
1. Reduce length of indexed columns to fix problem. 128 chars should be probably enough
2. Add uniqe paramater for column fully_qualified_name of table package

Additional changes:
* Test that column is unique in DB migration tests (on real databases)
* Introduced base for DB-related unit-tests. These tests use in-memory
  instance of SQLite
* Test that column is unique in DB-related unit-tests

Closes-Bug: #1339201
Closes-Bug: #1339728
Change-Id: I4816790e11f225c5dbb130747535094fdf06733e
This commit is contained in:
Ruslan Kamaldinov 2014-07-02 18:35:51 +04:00
parent 1f68419391
commit 2174fc8a84
6 changed files with 204 additions and 5 deletions

25
murano/db/api.py Normal file
View File

@ -0,0 +1,25 @@
# 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 murano.db import models
from murano.db import session
def setup_db():
engine = session.get_engine()
models.register_models(engine)
def drop_db():
engine = session.get_engine()
models.unregister_models(engine)

View File

@ -15,6 +15,7 @@
from oslo.config import cfg
from sqlalchemy import or_
from sqlalchemy.orm import attributes
# TODO(ruhe) use exception declared in openstack/common/db
from webob import exc
from murano.db import models

View File

@ -90,12 +90,14 @@ def upgrade():
sa.Column('created', sa.DateTime(), nullable=False),
sa.Column('updated', sa.DateTime(), nullable=False),
sa.Column('id', sa.String(length=36), nullable=False),
sa.Column('name', sa.String(length=80), nullable=False, index=True),
sa.Column('name', sa.String(length=80), nullable=False, unique=True),
sa.PrimaryKeyConstraint('id'),
mysql_engine=MYSQL_ENGINE,
mysql_charset=MYSQL_CHARSET
)
op.create_index('ix_category_name', 'category', ['name'])
op.create_table(
'apistats',
sa.Column('created', sa.DateTime(), nullable=False),
@ -137,8 +139,10 @@ def upgrade():
sa.Column('updated', sa.DateTime(), nullable=False),
sa.Column('id', sa.String(length=36), nullable=False),
sa.Column('archive', st.LargeBinary(), nullable=True),
sa.Column('fully_qualified_name', sa.String(length=512),
nullable=False, index=True),
sa.Column('fully_qualified_name',
sa.String(length=128),
nullable=False,
unique=True),
sa.Column('type', sa.String(length=20), nullable=False),
sa.Column('author', sa.String(length=80), nullable=True),
sa.Column('name', sa.String(length=80), nullable=False),
@ -153,6 +157,10 @@ def upgrade():
mysql_charset=MYSQL_CHARSET
)
op.create_index('ix_package_fqn',
'package',
['fully_qualified_name'])
op.create_table(
'session',
sa.Column('created', sa.DateTime(), nullable=False),
@ -189,7 +197,10 @@ def upgrade():
sa.Column('created', sa.DateTime(), nullable=False),
sa.Column('updated', sa.DateTime(), nullable=False),
sa.Column('id', sa.String(length=36), nullable=False),
sa.Column('name', sa.String(length=512), nullable=False, index=True),
sa.Column('name',
sa.String(length=128),
nullable=False,
unique=True),
sa.Column('package_id', sa.String(length=36), nullable=True),
sa.ForeignKeyConstraint(['package_id'], ['package.id'], ),
sa.PrimaryKeyConstraint('id'),
@ -197,6 +208,10 @@ def upgrade():
mysql_charset=MYSQL_CHARSET
)
op.create_index('ix_class_definition_name',
'class_definition',
['name'])
op.create_table(
'status',
sa.Column('created', sa.DateTime(), nullable=False),
@ -241,6 +256,10 @@ def upgrade():
def downgrade():
op.drop_index('ix_category_name', table_name='category')
op.drop_index('ix_package_fqn', table_name='package')
op.drop_index('ix_class_definition_name', table_name='class_definition')
op.drop_table('status')
op.drop_table('package_to_category')
op.drop_table('class_definition')

View File

@ -11,10 +11,35 @@
# See the License for the specific language governing permissions and
# limitations under the License.
import fixtures
from oslo.config import cfg
import testtools
from murano.db import api as db_api
from murano.openstack.common import log
CONF = cfg.CONF
CONF.import_opt('connection',
'murano.openstack.common.db.options',
group='database')
log.setup('murano')
class MuranoTestCase(testtools.TestCase):
def setUp(self):
super(MuranoTestCase, self).setUp()
self.useFixture(fixtures.FakeLogger('murano'))
def override_config(self, name, override, group=None):
CONF.set_override(name, override, group)
self.addCleanup(CONF.clear_override, name, group)
class MuranoWithDBTestCase(MuranoTestCase):
def setUp(self):
super(MuranoWithDBTestCase, self).setUp()
self.override_config('connection', "sqlite://", group='database')
db_api.setup_db()
self.addCleanup(db_api.drop_db)

View File

@ -11,13 +11,16 @@
# See the License for the specific language governing permissions and
# limitations under the License.
import datetime
import uuid
from oslo.config import cfg
from sqlalchemy import exc
from murano.db import models # noqa
from murano.openstack.common.db.sqlalchemy import utils as db_utils
from murano.tests.db.migration import test_migrations_base as base
CONF = cfg.CONF
@ -69,6 +72,43 @@ class TestMigrations(base.BaseWalkMigrationTestCase, base.CommonTestsMixIn):
def _check_001(self, engine, data):
self.assertColumnExists(engine, 'category', 'id')
self.assertColumnExists(engine, 'environment', 'tenant_id')
# make sure indexes are in place
self.assertIndexExists(engine,
'class_definition',
'ix_class_definition_name')
self.assertIndexExists(engine,
'package',
'ix_package_fqn')
self.assertIndexExists(engine,
'category',
'ix_category_name')
self._test_package_fqn_is_uniq(engine)
def _test_package_fqn_is_uniq(self, engine):
package_table = db_utils.get_table(engine, 'package')
package = {
'id': str(uuid.uuid4()),
'archive': "archive blob here",
'fully_qualified_name': 'com.example.package',
'type': 'class',
'author': 'OpenStack',
'name': 'package',
'enabled': True,
'description': 'some text',
'is_public': False,
'tags': ['tag1', 'tag2'],
'logo': "logo blob here",
'ui_definition': '{}',
'owner_id': '123',
'created': datetime.datetime.now(),
'updated': datetime.datetime.now()
}
package_table.insert().execute(package)
package['id'] = str(uuid.uuid4())
self.assertRaises(exc.IntegrityError,
package_table.insert().execute, package)

View File

@ -0,0 +1,89 @@
# 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 uuid
from webob import exc
from murano.db.catalog import api
from murano.openstack.common.db import exception as db_exception
from murano.tests import base
from murano.tests import utils
class CatalogDBTestCase(base.MuranoWithDBTestCase):
def setUp(self):
super(CatalogDBTestCase, self).setUp()
self.tenant_id = str(uuid.uuid4())
self.context = utils.dummy_context(tenant_id=self.tenant_id)
def _create_categories(self):
api.category_add('cat1')
api.category_add('cat2')
def _stub_package(self):
return {
'archive': "archive blob here",
'fully_qualified_name': 'com.example.package',
'type': 'class',
'author': 'OpenStack',
'name': 'package',
'enabled': True,
'description': 'some text',
'is_public': False,
'tags': ['tag1', 'tag2'],
'logo': "logo blob here",
'ui_definition': '{}',
}
def test_list_empty_categories(self):
res = api.category_get_names()
self.assertEqual(0, len(res))
def test_add_list_categories(self):
self._create_categories()
res = api.categories_list()
self.assertEqual(2, len(res))
for cat in res:
self.assertTrue(cat.id is not None)
self.assertTrue(cat.name.startswith('cat'))
def test_package_upload(self):
self._create_categories()
values = self._stub_package()
package = api.package_upload(values, self.tenant_id)
self.assertIsNotNone(package.id)
for k in values.keys():
self.assertEqual(values[k], package[k])
def test_package_fqn_is_unique(self):
self._create_categories()
values = self._stub_package()
api.package_upload(values, self.tenant_id)
self.assertRaises(db_exception.DBDuplicateEntry,
api.package_upload, values, self.tenant_id)
def test_package_delete(self):
values = self._stub_package()
package = api.package_upload(values, self.tenant_id)
api.package_delete(package.id, self.context)
self.assertRaises(exc.HTTPNotFound,
api.package_get, package.id, self.context)