Add migration 19 - move image location data

Migration 19 moves image location data from images.location to
the new image_locations table. The sqlalchemy db driver has been
updated to look at this new table, but the complexity of multiple
locations is not yet exposed to the reset of the system.

Related to bp multiple-image-locations

Change-Id: I6c4c684f522d9b41b9930e7f79a4e6fb3e3f5c39
This commit is contained in:
Brian Waldon 2013-01-29 13:43:32 -08:00
parent b0360709bd
commit fd9d069a6e
4 changed files with 187 additions and 4 deletions

View File

@ -266,6 +266,16 @@ def image_destroy(context, image_id):
return image_ref
def _limit_image_locations(image):
#NOTE(bcwaldon): mock this out until we support multiple images above
# the sqlalchemy db layer
if len(image.locations) > 0:
image.location = image.locations[0].value
else:
image.location = None
return image
def image_get(context, image_id, session=None, force_show_deleted=False):
"""Get an image or raise if it does not exist."""
session = session or get_session()
@ -273,6 +283,7 @@ def image_get(context, image_id, session=None, force_show_deleted=False):
try:
query = session.query(models.Image)\
.options(sa_orm.joinedload(models.Image.properties))\
.options(sa_orm.joinedload(models.Image.locations))\
.filter_by(id=image_id)
# filter out deleted images if context disallows it
@ -288,7 +299,11 @@ def image_get(context, image_id, session=None, force_show_deleted=False):
if not is_image_visible(context, image):
raise exception.Forbidden("Image not visible to you")
return image
#NOTE(bcwaldon): mock this out until we support multiple images above
# the sqlalchemy db layer
image.location = _image_location_get(image_id, session)
return _limit_image_locations(image)
def is_image_mutable(context, image):
@ -484,7 +499,8 @@ def image_get_all(context, filters=None, marker=None, limit=None,
session = get_session()
query = session.query(models.Image)\
.options(sa_orm.joinedload(models.Image.properties))
.options(sa_orm.joinedload(models.Image.properties))\
.options(sa_orm.joinedload(models.Image.locations))
# NOTE(markwash) treat is_public=None as if it weren't filtered
if 'is_public' in filters and filters['is_public'] is None:
@ -554,7 +570,7 @@ def image_get_all(context, filters=None, marker=None, limit=None,
marker=marker_image,
sort_dir=sort_dir)
return query.all()
return [_limit_image_locations(image) for image in query.all()]
def _drop_protected_attrs(model_class, values):
@ -613,6 +629,12 @@ def _image_update(context, values, image_id, purge_props=False):
# not a dict.
properties = values.pop('properties', {})
try:
location = values.pop('location')
location_provided = True
except KeyError:
location_provided = False
if image_id:
image_ref = image_get(context, image_id, session=session)
@ -660,9 +682,36 @@ def _image_update(context, values, image_id, purge_props=False):
_set_properties_for_image(context, image_ref, properties, purge_props,
session)
if location_provided:
_image_location_set(image_ref.id, location, session)
return image_get(context, image_ref.id)
def _image_location_get(image_id, session):
location = session.query(models.ImageLocation)\
.filter_by(image_id=image_id)\
.filter_by(deleted=False)\
.first()
try:
return location['value']
except TypeError:
return None
def _image_location_set(image_id, location, session):
locations = session.query(models.ImageLocation)\
.filter_by(image_id=image_id)\
.filter_by(deleted=False)\
.all()
for location_ref in locations:
location_ref.delete(session=session)
if location is not None:
location_ref = models.ImageLocation(image_id=image_id, value=location)
location_ref.save()
def _set_properties_for_image(context, image_ref, properties,
purge_props=False, session=None):
"""

View File

@ -0,0 +1,69 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2013 OpenStack Foundation
# 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 sqlalchemy
import logging as base_logging
import glance.openstack.common.log as logging
LOG = logging.getLogger(__name__)
sa_logger = base_logging.getLogger('sqlalchemy.engine')
sa_logger.setLevel(base_logging.DEBUG)
def get_images_table(meta):
return sqlalchemy.Table('images', meta, autoload=True)
def get_image_locations_table(meta):
return sqlalchemy.Table('image_locations', meta, autoload=True)
def upgrade(migrate_engine):
meta = sqlalchemy.schema.MetaData(migrate_engine)
images_table = get_images_table(meta)
image_locations_table = get_image_locations_table(meta)
image_records = images_table.select().execute().fetchall()
for image in image_records:
if image.location is not None:
values = {
'image_id': image.id,
'value': image.location,
'created_at': image.created_at,
'updated_at': image.updated_at,
'deleted': image.deleted,
'deleted_at': image.deleted_at,
}
image_locations_table.insert(values=values).execute()
def downgrade(migrate_engine):
meta = sqlalchemy.schema.MetaData(migrate_engine)
images_table = get_images_table(meta)
image_locations_table = get_image_locations_table(meta)
image_records = image_locations_table.select().execute().fetchall()
for image_location in image_records:
images_table.update(values={'location': image_location.value})\
.where(images_table.c.id == image_location.image_id)\
.execute()

View File

@ -108,7 +108,6 @@ class Image(BASE, ModelBase):
size = Column(BigInteger)
status = Column(String(30), nullable=False)
is_public = Column(Boolean, nullable=False, default=False)
location = Column(Text)
checksum = Column(String(32))
min_disk = Column(Integer(), nullable=False, default=0)
min_ram = Column(Integer(), nullable=False, default=0)
@ -139,6 +138,16 @@ class ImageTag(BASE, ModelBase):
value = Column(String(255), nullable=False)
class ImageLocation(BASE, ModelBase):
"""Represents an image location in the datastore"""
__tablename__ = 'image_locations'
id = Column(Integer, primary_key=True, nullable=False)
image_id = Column(String(36), ForeignKey('images.id'), nullable=False)
image = relationship(Image, backref=backref('locations'))
value = Column(Text(), nullable=False)
class ImageMember(BASE, ModelBase):
"""Represents an image members in the datastore"""
__tablename__ = 'image_members'

View File

@ -606,3 +606,59 @@ class TestMigrations(utils.BaseTestCase):
migration_api.downgrade(16)
assert_locations()
def test_migration_19(self):
for key, engine in self.engines.items():
self.config(sql_connection=TestMigrations.TEST_DATABASES[key])
migration_api.version_control(version=0)
migration_api.upgrade(18)
images_table = Table('images', MetaData(engine), autoload=True)
now = datetime.datetime.now()
base_values = {
'deleted': False,
'created_at': now,
'updated_at': now,
'status': 'active',
'is_public': True,
'min_disk': 0,
'min_ram': 0,
}
images = [
{'id': 1, 'location': 'http://glance.example.com'},
#NOTE(bcwaldon): images with a location of None should
# not be migrated
{'id': 2, 'location': None},
]
map(lambda image: image.update(base_values), images)
for image in images:
images_table.insert().values(image).execute()
migration_api.upgrade(19)
image_locations_table = Table('image_locations', MetaData(engine),
autoload=True)
records = image_locations_table.select().execute().fetchall()
self.assertEqual(len(records), 1)
locations = dict([(il.image_id, il.value) for il in records])
self.assertEqual({'1': 'http://glance.example.com'}, locations)
image_locations_table = Table('image_locations', MetaData(engine),
autoload=True)
image_locations_table.update()\
.where(image_locations_table.c.image_id == 1)\
.values(value='http://swift.example.com')\
.execute()
migration_api.downgrade(18)
images_table = Table('images', MetaData(engine), autoload=True)
records = images_table.select().execute().fetchall()
self.assertEqual(len(records), 2)
locations = dict([(i.id, i.location) for i in records])
self.assertEqual({'1': 'http://swift.example.com', '2': None},
locations)