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
|
||||
# under the License.
|
||||
|
||||
from glance.common import exception
|
||||
import glance.domain
|
||||
from glance.openstack.common import cfg
|
||||
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',
|
||||
'location', 'checksum', 'owner',
|
||||
'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
|
||||
else:
|
||||
# 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):
|
||||
if image['id'] == marker:
|
||||
@ -191,8 +191,7 @@ def _sort_images(images, sort_key, sort_dir):
|
||||
return images
|
||||
|
||||
|
||||
@log_call
|
||||
def image_get(context, image_id, session=None, force_show_deleted=False):
|
||||
def _image_get(context, image_id, force_show_deleted=False):
|
||||
try:
|
||||
image = DATA['images'][image_id]
|
||||
except KeyError:
|
||||
@ -210,6 +209,12 @@ def image_get(context, image_id, session=None, force_show_deleted=False):
|
||||
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
|
||||
def image_get_all(context, filters=None, marker=None, limit=None,
|
||||
sort_key='created_at', sort_dir='desc'):
|
||||
@ -224,7 +229,7 @@ def image_get_all(context, filters=None, marker=None, limit=None,
|
||||
|
||||
@log_call
|
||||
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'],
|
||||
values['name'],
|
||||
values['value'])
|
||||
@ -351,13 +356,14 @@ def image_destroy(context, image_id):
|
||||
try:
|
||||
DATA['images'][image_id]['deleted'] = True
|
||||
DATA['images'][image_id]['deleted_at'] = timeutils.utcnow()
|
||||
return copy.deepcopy(DATA['images'][image_id])
|
||||
except KeyError:
|
||||
raise exception.NotFound()
|
||||
|
||||
|
||||
@log_call
|
||||
def image_tag_get_all(context, image_id):
|
||||
image_get(context, image_id)
|
||||
_image_get(context, 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
|
||||
check_mutate_authorization(context, image_ref)
|
||||
else:
|
||||
if 'size' in values:
|
||||
if values.get('size') is not None:
|
||||
values['size'] = int(values['size'])
|
||||
|
||||
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