diff --git a/glance/common/db/sqlalchemy/session.py b/glance/common/db/sqlalchemy/session.py index 8837e28a4b..41cf14f29f 100644 --- a/glance/common/db/sqlalchemy/session.py +++ b/glance/common/db/sqlalchemy/session.py @@ -29,14 +29,20 @@ _ENGINE = None _MAKER = None + +def get_engine(echo=False): + global _ENGINE + if not _ENGINE: + _ENGINE = create_engine(FLAGS.sql_connection, echo=echo) + return _ENGINE + + def get_session(autocommit=True, expire_on_commit=False): """Helper method to grab session""" - global _ENGINE global _MAKER if not _MAKER: - if not _ENGINE: - _ENGINE = create_engine(FLAGS.sql_connection, echo=False) - _MAKER = sessionmaker(bind=_ENGINE, + engine = get_engine() + _MAKER = sessionmaker(bind=engine, autocommit=autocommit, expire_on_commit=expire_on_commit) return _MAKER() diff --git a/glance/parallax/db/sqlalchemy/models.py b/glance/parallax/db/sqlalchemy/models.py index 5b4e75116b..17ed33b7ed 100644 --- a/glance/parallax/db/sqlalchemy/models.py +++ b/glance/parallax/db/sqlalchemy/models.py @@ -26,9 +26,10 @@ from sqlalchemy.orm import relationship, backref, exc, object_mapper, validates from sqlalchemy import Column, Integer, String from sqlalchemy import ForeignKey, DateTime, Boolean, Text +from sqlalchemy import UniqueConstraint from sqlalchemy.ext.declarative import declarative_base -from glance.common.db.sqlalchemy.session import get_session +from glance.common.db.sqlalchemy.session import get_session, get_engine from glance.common import exception from glance.common import flags @@ -133,14 +134,15 @@ class Image(BASE, ModelBase): @validates('image_type') def validate_image_type(self, key, image_type): if not image_type in ('machine', 'kernel', 'ramdisk', 'raw'): - raise exception.Invalid("Invalid image type '%s' for image." % image_type) + raise exception.Invalid( + "Invalid image type '%s' for image." % image_type) return image_type @validates('status') - def validate_status(self, key, state): - if not state in ('available', 'pending', 'disabled'): + def validate_status(self, key, status): + if not status in ('available', 'pending', 'disabled'): raise exception.Invalid("Invalid status '%s' for image." % status) - return image_type + return status # TODO(sirp): should these be stored as metadata? #user_id = Column(String(255)) @@ -176,18 +178,24 @@ class ImageMetadatum(BASE, ModelBase): """Represents an image metadata in the datastore""" __tablename__ = 'image_metadata' __prefix__ = 'img-meta' + __table_args__ = (UniqueConstraint('image_id', 'key'), {}) + id = Column(Integer, primary_key=True) image_id = Column(Integer, ForeignKey('images.id'), nullable=False) image = relationship(Image, backref=backref('metadata')) - key = Column(String(255), index=True, unique=True) + key = Column(String(255), index=True) value = Column(Text) def register_models(): """Register Models and create metadata""" - from sqlalchemy import create_engine - models = (Image, ImageFile, ImageMetadatum) - engine = create_engine(FLAGS.sql_connection, echo=False) - for model in models: - model.metadata.create_all(engine) + engine = get_engine() + BASE.metadata.create_all(engine) + + +def unregister_models(): + """Unregister Models, useful clearing out data before testing""" + engine = get_engine() + BASE.metadata.drop_all(engine) + diff --git a/tests/unit/test_parallax_models.py b/tests/unit/test_parallax_models.py new file mode 100644 index 0000000000..3439ad2aeb --- /dev/null +++ b/tests/unit/test_parallax_models.py @@ -0,0 +1,77 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2010 OpenStack, LLC +# 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 unittest +import sqlalchemy.exceptions as sa_exc + +from glance.common import exception +from glance.parallax import db +from glance.common import flags +from glance.parallax.db.sqlalchemy import models + +FLAGS = flags.FLAGS + + +class TestModels(unittest.TestCase): + """ Test Parllax SQLAlchemy models using an in-memory sqlite DB""" + + def setUp(self): + FLAGS.sql_connection = "sqlite://" # in-memory db + models.unregister_models() + models.register_models() + self.image = self._make_image(id=2, name='fake image #2') + + def test_metadata_key_constraint_ok(self): + """Two different images are permitted to have metadata that share the + same key + + """ + self._make_metadatum(self.image, key="spam", value="eggs") + + second_image = self._make_image(id=3, name='fake image #3') + self._make_metadatum(second_image, key="spam", value="eggs") + + def test_metadata_key_constraint_bad(self): + """The same image cannot have two distinct pieces of metadata with the + same key. + + """ + self._make_metadatum(self.image, key="spam", value="eggs") + + self.assertRaises(sa_exc.IntegrityError, + self._make_metadatum, self.image, key="spam", value="eggs") + + def _make_image(self, id, name): + """Convenience method to create an image with a given name and id""" + fixture = {'id': id, + 'name': name, + 'is_public': True, + 'image_type': 'kernel', + 'status': 'available'} + + context = None + image = db.api.image_create(context, fixture) + return image + + def _make_metadatum(self, image, key, value): + """Convenience method to create metadata attached to an image""" + metadata = {'image_id': image['id'], 'key': key, 'value': value} + context = None + metadatum = db.api.image_metadatum_create(context, metadata) + return metadatum + +