Add an image repo to encapsulate db api access
This patch adds glance.db.ImageRepo which provides a Repository-like abstraction over image persistence. In the process of testing, a few additional changes to the simple and sqlalchemy db apis were needed. Partially implements bp:glance-domain-logic-layer Change-Id: Ie35e427aa8fc7f3bece8d4d383afde4cc02b2096
This commit is contained in:
parent
6d57df0eda
commit
b46563eda7
@ -17,6 +17,8 @@
|
|||||||
# License for the specific language governing permissions and limitations
|
# License for the specific language governing permissions and limitations
|
||||||
# under the License.
|
# under the License.
|
||||||
|
|
||||||
|
from glance.common import exception
|
||||||
|
import glance.domain
|
||||||
from glance.openstack.common import cfg
|
from glance.openstack.common import cfg
|
||||||
from glance.openstack.common import importutils
|
from glance.openstack.common import importutils
|
||||||
|
|
||||||
@ -56,3 +58,123 @@ IMAGE_ATTRS = BASE_MODEL_ATTRS | set(['name', 'status', 'size',
|
|||||||
'min_disk', 'min_ram', 'is_public',
|
'min_disk', 'min_ram', 'is_public',
|
||||||
'location', 'checksum', 'owner',
|
'location', 'checksum', 'owner',
|
||||||
'protected'])
|
'protected'])
|
||||||
|
|
||||||
|
|
||||||
|
class ImageRepo(object):
|
||||||
|
|
||||||
|
def __init__(self, context, db_api):
|
||||||
|
self.context = context
|
||||||
|
self.db_api = db_api
|
||||||
|
|
||||||
|
def get(self, image_id):
|
||||||
|
try:
|
||||||
|
db_api_image = dict(self.db_api.image_get(self.context, image_id))
|
||||||
|
assert not db_api_image['deleted']
|
||||||
|
except (exception.NotFound, exception.Forbidden, AssertionError):
|
||||||
|
raise exception.NotFound(image_id=image_id)
|
||||||
|
tags = self.db_api.image_tag_get_all(self.context, image_id)
|
||||||
|
image = self._format_image_from_db(db_api_image, tags)
|
||||||
|
return image
|
||||||
|
|
||||||
|
def list(self, marker=None, limit=None, sort_key='created_at',
|
||||||
|
sort_dir='desc', filters=None):
|
||||||
|
db_filters = self._translate_filters(filters)
|
||||||
|
db_api_images = self.db_api.image_get_all(
|
||||||
|
self.context, filters=db_filters, marker=marker, limit=limit,
|
||||||
|
sort_key=sort_key, sort_dir=sort_dir)
|
||||||
|
images = []
|
||||||
|
for db_api_image in db_api_images:
|
||||||
|
tags = self.db_api.image_tag_get_all(self.context,
|
||||||
|
db_api_image['id'])
|
||||||
|
image = self._format_image_from_db(dict(db_api_image), tags)
|
||||||
|
images.append(image)
|
||||||
|
return images
|
||||||
|
|
||||||
|
def _translate_filters(self, filters):
|
||||||
|
db_filters = {}
|
||||||
|
if filters is None:
|
||||||
|
return None
|
||||||
|
for key, value in filters.iteritems():
|
||||||
|
if key == 'visibility':
|
||||||
|
db_filters['is_public'] = value == 'public'
|
||||||
|
else:
|
||||||
|
db_filters[key] = value
|
||||||
|
return db_filters
|
||||||
|
|
||||||
|
def _format_image_from_db(self, db_image, db_tags):
|
||||||
|
visibility = 'public' if db_image['is_public'] else 'private'
|
||||||
|
properties = {}
|
||||||
|
for prop in db_image.pop('properties'):
|
||||||
|
# NOTE(markwash) db api requires us to filter deleted
|
||||||
|
if not prop['deleted']:
|
||||||
|
properties[prop['name']] = prop['value']
|
||||||
|
return glance.domain.Image(
|
||||||
|
image_id=db_image['id'],
|
||||||
|
name=db_image['name'],
|
||||||
|
status=db_image['status'],
|
||||||
|
created_at=db_image['created_at'],
|
||||||
|
updated_at=db_image['updated_at'],
|
||||||
|
visibility=visibility,
|
||||||
|
min_disk=db_image['min_disk'],
|
||||||
|
min_ram=db_image['min_ram'],
|
||||||
|
protected=db_image['protected'],
|
||||||
|
location=db_image['location'],
|
||||||
|
checksum=db_image['checksum'],
|
||||||
|
owner=db_image['owner'],
|
||||||
|
disk_format=db_image['disk_format'],
|
||||||
|
container_format=db_image['container_format'],
|
||||||
|
size=db_image['size'],
|
||||||
|
extra_properties=properties,
|
||||||
|
tags=db_tags
|
||||||
|
)
|
||||||
|
|
||||||
|
def _format_image_to_db(self, image):
|
||||||
|
return {
|
||||||
|
'id': image.image_id,
|
||||||
|
'name': image.name,
|
||||||
|
'status': image.status,
|
||||||
|
'created_at': image.created_at,
|
||||||
|
'min_disk': image.min_disk,
|
||||||
|
'min_ram': image.min_ram,
|
||||||
|
'protected': image.protected,
|
||||||
|
'location': image.location,
|
||||||
|
'checksum': image.checksum,
|
||||||
|
'owner': image.owner,
|
||||||
|
'disk_format': image.disk_format,
|
||||||
|
'container_format': image.container_format,
|
||||||
|
'size': image.size,
|
||||||
|
'is_public': image.visibility == 'public',
|
||||||
|
'properties': dict(image.extra_properties),
|
||||||
|
}
|
||||||
|
|
||||||
|
def add(self, image):
|
||||||
|
image_values = self._format_image_to_db(image)
|
||||||
|
new_values = self.db_api.image_create(self.context, image_values)
|
||||||
|
self.db_api.image_tag_set_all(self.context,
|
||||||
|
image.image_id, image.tags)
|
||||||
|
image.created_at = new_values['created_at']
|
||||||
|
image.updated_at = new_values['updated_at']
|
||||||
|
|
||||||
|
def save(self, image):
|
||||||
|
image_values = self._format_image_to_db(image)
|
||||||
|
try:
|
||||||
|
new_values = self.db_api.image_update(self.context,
|
||||||
|
image.image_id,
|
||||||
|
image_values,
|
||||||
|
purge_props=True)
|
||||||
|
except (exception.NotFound, exception.Forbidden):
|
||||||
|
raise exception.NotFound(image_id=image.image_id)
|
||||||
|
self.db_api.image_tag_set_all(self.context, image.image_id,
|
||||||
|
image.tags)
|
||||||
|
image.updated_at = new_values['updated_at']
|
||||||
|
|
||||||
|
def remove(self, image):
|
||||||
|
image_values = self._format_image_to_db(image)
|
||||||
|
try:
|
||||||
|
self.db_api.image_update(self.context, image.image_id,
|
||||||
|
image_values, purge_props=True)
|
||||||
|
except (exception.NotFound, exception.Forbidden):
|
||||||
|
raise exception.NotFound(image_id=image.image_id)
|
||||||
|
# NOTE(markwash): don't update tags?
|
||||||
|
new_values = self.db_api.image_destroy(self.context, image.image_id)
|
||||||
|
image.updated_at = new_values['updated_at']
|
||||||
|
@ -167,7 +167,7 @@ def _do_pagination(context, images, marker, limit, show_deleted):
|
|||||||
start = 0
|
start = 0
|
||||||
else:
|
else:
|
||||||
# Check that the image is accessible
|
# Check that the image is accessible
|
||||||
image_get(context, marker, force_show_deleted=show_deleted)
|
_image_get(context, marker, force_show_deleted=show_deleted)
|
||||||
|
|
||||||
for i, image in enumerate(images):
|
for i, image in enumerate(images):
|
||||||
if image['id'] == marker:
|
if image['id'] == marker:
|
||||||
@ -191,8 +191,7 @@ def _sort_images(images, sort_key, sort_dir):
|
|||||||
return images
|
return images
|
||||||
|
|
||||||
|
|
||||||
@log_call
|
def _image_get(context, image_id, force_show_deleted=False):
|
||||||
def image_get(context, image_id, session=None, force_show_deleted=False):
|
|
||||||
try:
|
try:
|
||||||
image = DATA['images'][image_id]
|
image = DATA['images'][image_id]
|
||||||
except KeyError:
|
except KeyError:
|
||||||
@ -210,6 +209,12 @@ def image_get(context, image_id, session=None, force_show_deleted=False):
|
|||||||
return image
|
return image
|
||||||
|
|
||||||
|
|
||||||
|
@log_call
|
||||||
|
def image_get(context, image_id, session=None, force_show_deleted=False):
|
||||||
|
image = _image_get(context, image_id, force_show_deleted)
|
||||||
|
return copy.deepcopy(image)
|
||||||
|
|
||||||
|
|
||||||
@log_call
|
@log_call
|
||||||
def image_get_all(context, filters=None, marker=None, limit=None,
|
def image_get_all(context, filters=None, marker=None, limit=None,
|
||||||
sort_key='created_at', sort_dir='desc'):
|
sort_key='created_at', sort_dir='desc'):
|
||||||
@ -224,7 +229,7 @@ def image_get_all(context, filters=None, marker=None, limit=None,
|
|||||||
|
|
||||||
@log_call
|
@log_call
|
||||||
def image_property_create(context, values):
|
def image_property_create(context, values):
|
||||||
image = image_get(context, values['image_id'])
|
image = _image_get(context, values['image_id'])
|
||||||
prop = _image_property_format(values['image_id'],
|
prop = _image_property_format(values['image_id'],
|
||||||
values['name'],
|
values['name'],
|
||||||
values['value'])
|
values['value'])
|
||||||
@ -351,13 +356,14 @@ def image_destroy(context, image_id):
|
|||||||
try:
|
try:
|
||||||
DATA['images'][image_id]['deleted'] = True
|
DATA['images'][image_id]['deleted'] = True
|
||||||
DATA['images'][image_id]['deleted_at'] = timeutils.utcnow()
|
DATA['images'][image_id]['deleted_at'] = timeutils.utcnow()
|
||||||
|
return copy.deepcopy(DATA['images'][image_id])
|
||||||
except KeyError:
|
except KeyError:
|
||||||
raise exception.NotFound()
|
raise exception.NotFound()
|
||||||
|
|
||||||
|
|
||||||
@log_call
|
@log_call
|
||||||
def image_tag_get_all(context, image_id):
|
def image_tag_get_all(context, image_id):
|
||||||
image_get(context, image_id)
|
_image_get(context, image_id)
|
||||||
return DATA['tags'].get(image_id, [])
|
return DATA['tags'].get(image_id, [])
|
||||||
|
|
||||||
|
|
||||||
|
@ -579,7 +579,7 @@ def _image_update(context, values, image_id, purge_props=False):
|
|||||||
# Perform authorization check
|
# Perform authorization check
|
||||||
check_mutate_authorization(context, image_ref)
|
check_mutate_authorization(context, image_ref)
|
||||||
else:
|
else:
|
||||||
if 'size' in values:
|
if values.get('size') is not None:
|
||||||
values['size'] = int(values['size'])
|
values['size'] = int(values['size'])
|
||||||
|
|
||||||
if 'min_ram' in values:
|
if 'min_ram' in values:
|
||||||
|
176
glance/tests/unit/test_db.py
Normal file
176
glance/tests/unit/test_db.py
Normal file
@ -0,0 +1,176 @@
|
|||||||
|
# Copyright 2012 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 glance.context
|
||||||
|
from glance.common import exception
|
||||||
|
import glance.db
|
||||||
|
from glance.openstack.common import uuidutils
|
||||||
|
import glance.tests.unit.utils as unit_test_utils
|
||||||
|
import glance.tests.utils as test_utils
|
||||||
|
|
||||||
|
|
||||||
|
UUID1 = 'c80a1a6c-bd1f-41c5-90ee-81afedb1d58d'
|
||||||
|
UUID2 = 'a85abd86-55b3-4d5b-b0b4-5d0a6e6042fc'
|
||||||
|
UUID3 = '971ec09a-8067-4bc8-a91f-ae3557f1c4c7'
|
||||||
|
UUID4 = '6bbe7cc2-eae7-4c0f-b50d-a7160b0c6a86'
|
||||||
|
|
||||||
|
TENANT1 = '6838eb7b-6ded-434a-882c-b344c77fe8df'
|
||||||
|
TENANT2 = '2c014f32-55eb-467d-8fcb-4bd706012f81'
|
||||||
|
TENANT3 = '5a3e60e8-cfa9-4a9e-a90a-62b42cea92b8'
|
||||||
|
TENANT4 = 'c6c87f25-8a94-47ed-8c83-053c25f42df4'
|
||||||
|
|
||||||
|
USER1 = '54492ba0-f4df-4e4e-be62-27f4d76b29cf'
|
||||||
|
|
||||||
|
|
||||||
|
def _db_fixture(id, **kwargs):
|
||||||
|
obj = {
|
||||||
|
'id': id,
|
||||||
|
'name': None,
|
||||||
|
'is_public': False,
|
||||||
|
'properties': {},
|
||||||
|
'checksum': None,
|
||||||
|
'owner': None,
|
||||||
|
'status': 'queued',
|
||||||
|
'tags': [],
|
||||||
|
'size': None,
|
||||||
|
'location': None,
|
||||||
|
'protected': False,
|
||||||
|
'disk_format': None,
|
||||||
|
'container_format': None,
|
||||||
|
'deleted': False,
|
||||||
|
'min_ram': None,
|
||||||
|
'min_disk': None,
|
||||||
|
}
|
||||||
|
obj.update(kwargs)
|
||||||
|
return obj
|
||||||
|
|
||||||
|
|
||||||
|
class TestImageRepo(test_utils.BaseTestCase):
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
self.db = unit_test_utils.FakeDB()
|
||||||
|
self.db.reset()
|
||||||
|
self.context = glance.context.RequestContext(
|
||||||
|
user=USER1, tenant=TENANT1)
|
||||||
|
self.image_repo = glance.db.ImageRepo(self.context, self.db)
|
||||||
|
self.image_factory = glance.domain.ImageFactory()
|
||||||
|
self._create_images()
|
||||||
|
super(TestImageRepo, self).setUp()
|
||||||
|
|
||||||
|
def _create_images(self):
|
||||||
|
self.db.reset()
|
||||||
|
self.images = [
|
||||||
|
_db_fixture(UUID1, owner=TENANT1, name='1', size=256,
|
||||||
|
is_public=True, status='active'),
|
||||||
|
_db_fixture(UUID2, owner=TENANT1, name='2',
|
||||||
|
size=512, is_public=False),
|
||||||
|
_db_fixture(UUID3, owner=TENANT3, name='3',
|
||||||
|
size=1024, is_public=True),
|
||||||
|
_db_fixture(UUID4, owner=TENANT4, name='4', size=2048),
|
||||||
|
]
|
||||||
|
[self.db.image_create(None, image) for image in self.images]
|
||||||
|
|
||||||
|
self.db.image_tag_set_all(None, UUID1, ['ping', 'pong'])
|
||||||
|
|
||||||
|
def test_get(self):
|
||||||
|
image = self.image_repo.get(UUID1)
|
||||||
|
self.assertEquals(image.image_id, UUID1)
|
||||||
|
self.assertEquals(image.name, '1')
|
||||||
|
self.assertEquals(image.tags, set(['ping', 'pong']))
|
||||||
|
self.assertEquals(image.visibility, 'public')
|
||||||
|
self.assertEquals(image.status, 'active')
|
||||||
|
self.assertEquals(image.size, 256)
|
||||||
|
self.assertEquals(image.owner, TENANT1)
|
||||||
|
|
||||||
|
def test_get_not_found(self):
|
||||||
|
self.assertRaises(exception.NotFound, self.image_repo.get,
|
||||||
|
uuidutils.generate_uuid())
|
||||||
|
|
||||||
|
def test_get_forbidden(self):
|
||||||
|
self.assertRaises(exception.NotFound, self.image_repo.get, UUID4)
|
||||||
|
|
||||||
|
def test_list(self):
|
||||||
|
images = self.image_repo.list()
|
||||||
|
image_ids = set([i.image_id for i in images])
|
||||||
|
self.assertEqual(set([UUID1, UUID2, UUID3]), image_ids)
|
||||||
|
|
||||||
|
def test_list_with_marker(self):
|
||||||
|
full_images = self.image_repo.list()
|
||||||
|
full_ids = [i.image_id for i in full_images]
|
||||||
|
marked_images = self.image_repo.list(marker=full_ids[0])
|
||||||
|
actual_ids = [i.image_id for i in marked_images]
|
||||||
|
self.assertEqual(actual_ids, full_ids[1:])
|
||||||
|
|
||||||
|
def test_list_with_last_marker(self):
|
||||||
|
images = self.image_repo.list()
|
||||||
|
marked_images = self.image_repo.list(marker=images[-1].image_id)
|
||||||
|
self.assertEqual(len(marked_images), 0)
|
||||||
|
|
||||||
|
def test_limited_list(self):
|
||||||
|
limited_images = self.image_repo.list(limit=2)
|
||||||
|
self.assertEqual(len(limited_images), 2)
|
||||||
|
|
||||||
|
def test_list_with_marker_and_limit(self):
|
||||||
|
full_images = self.image_repo.list()
|
||||||
|
full_ids = [i.image_id for i in full_images]
|
||||||
|
marked_images = self.image_repo.list(marker=full_ids[0], limit=1)
|
||||||
|
actual_ids = [i.image_id for i in marked_images]
|
||||||
|
self.assertEqual(actual_ids, full_ids[1:2])
|
||||||
|
|
||||||
|
def test_list_private_images(self):
|
||||||
|
filters = {'visibility': 'private'}
|
||||||
|
images = self.image_repo.list(filters=filters)
|
||||||
|
image_ids = set([i.image_id for i in images])
|
||||||
|
self.assertEqual(set([UUID2]), image_ids)
|
||||||
|
|
||||||
|
def test_list_public_images(self):
|
||||||
|
filters = {'visibility': 'public'}
|
||||||
|
images = self.image_repo.list(filters=filters)
|
||||||
|
image_ids = set([i.image_id for i in images])
|
||||||
|
self.assertEqual(set([UUID1, UUID3]), image_ids)
|
||||||
|
|
||||||
|
def test_sorted_list(self):
|
||||||
|
images = self.image_repo.list(sort_key='size', sort_dir='asc')
|
||||||
|
image_ids = [i.image_id for i in images]
|
||||||
|
self.assertEqual([UUID1, UUID2, UUID3], image_ids)
|
||||||
|
|
||||||
|
def test_add_image(self):
|
||||||
|
image = self.image_factory.new_image(name='added image')
|
||||||
|
self.assertEqual(image.updated_at, image.created_at)
|
||||||
|
self.image_repo.add(image)
|
||||||
|
self.assertTrue(image.updated_at > image.created_at)
|
||||||
|
retreived_image = self.image_repo.get(image.image_id)
|
||||||
|
self.assertEqual(retreived_image.name, 'added image')
|
||||||
|
self.assertEqual(retreived_image.updated_at, image.updated_at)
|
||||||
|
|
||||||
|
def test_save_image(self):
|
||||||
|
image = self.image_repo.get(UUID1)
|
||||||
|
original_update_time = image.updated_at
|
||||||
|
image.name = 'foo'
|
||||||
|
image.tags = ['king', 'kong']
|
||||||
|
self.image_repo.save(image)
|
||||||
|
current_update_time = image.updated_at
|
||||||
|
self.assertTrue(current_update_time > original_update_time)
|
||||||
|
image = self.image_repo.get(UUID1)
|
||||||
|
self.assertEqual(image.name, 'foo')
|
||||||
|
self.assertEqual(image.tags, set(['king', 'kong']))
|
||||||
|
self.assertEqual(image.updated_at, current_update_time)
|
||||||
|
|
||||||
|
def test_remove_image(self):
|
||||||
|
image = self.image_repo.get(UUID1)
|
||||||
|
previous_update_time = image.updated_at
|
||||||
|
self.image_repo.remove(image)
|
||||||
|
self.assertTrue(image.updated_at > previous_update_time)
|
||||||
|
self.assertRaises(exception.NotFound, self.image_repo.get, UUID1)
|
Loading…
Reference in New Issue
Block a user