Update db layer to expose multiple image locations

* The DB API now exposes a 'locations' image attribute rather than
  'location'. The new field is guaranteed to be a list of zero of
  more items
* The v1 and v2 APIs only look for the first item in the list of
  locations.
* Related to bp multiple-image-locations

Change-Id: I830b383d8a8e50a01e461658fb9abe384de1a353
This commit is contained in:
Brian Waldon 2013-02-27 11:13:40 -08:00 committed by Mark J. Washenberger
parent 4028e74ef4
commit 222a00fef9
21 changed files with 133 additions and 107 deletions

View File

@ -227,7 +227,7 @@ class ImmutableImageProxy(object):
min_disk = _immutable_attr('base', 'min_disk')
min_ram = _immutable_attr('base', 'min_ram')
protected = _immutable_attr('base', 'protected')
location = _immutable_attr('base', 'location')
locations = _immutable_attr('base', 'locations')
checksum = _immutable_attr('base', 'checksum')
owner = _immutable_attr('base', 'owner')
disk_format = _immutable_attr('base', 'disk_format')

View File

@ -20,10 +20,9 @@ from glance.common import exception
def update_image_read_acl(req, store_api, db_api, image):
"""Helper function to set ACL permissions on images in the image store"""
location_uri = image['location']
public = image['is_public']
image_id = image['id']
if location_uri:
if image['locations']:
try:
read_tenants = []
write_tenants = []
@ -34,9 +33,10 @@ def update_image_read_acl(req, store_api, db_api, image):
write_tenants.append(member['member'])
else:
read_tenants.append(member['member'])
store_api.set_acls(req.context, location_uri, public=public,
read_tenants=read_tenants,
write_tenants=write_tenants)
for location in image['locations']:
store_api.set_acls(req.context, location, public=public,
read_tenants=read_tenants,
write_tenants=write_tenants)
except exception.UnknownScheme:
msg = _("Store for image_id not found: %s") % image_id
raise webob.exc.HTTPBadRequest(explanation=msg,

View File

@ -93,7 +93,7 @@ class ImageDataController(object):
except exception.Forbidden as e:
raise webob.exc.HTTPForbidden(explanation=unicode(e))
if not image.location:
if not image.locations:
reason = _("No image data could be found")
raise webob.exc.HTTPNotFound(reason)
return image

View File

@ -152,10 +152,9 @@ class ImageMembersController(object):
raise webob.exc.HTTPForbidden(explanation=unicode(e))
def _update_store_acls(self, req, image):
location_uri = image.location
public = image.visibility == 'public'
member_repo = image.get_member_repo()
if location_uri:
if image.locations:
try:
read_tenants = []
write_tenants = []
@ -163,9 +162,10 @@ class ImageMembersController(object):
if members:
for member in members:
read_tenants.append(member.member_id)
glance.store.set_acls(req.context, location_uri, public=public,
read_tenants=read_tenants,
write_tenants=write_tenants)
for location in image.locations:
glance.store.set_acls(req.context, location, public=public,
read_tenants=read_tenants,
write_tenants=write_tenants)
except exception.UnknownScheme:
msg = _("Store for image not found: %s") % image_id
raise webob.exc.HTTPBadRequest(explanation=msg,

View File

@ -173,8 +173,8 @@ class RequestDeserializer(wsgi.JSONRequestDeserializer):
_disallowed_properties = ['direct_url', 'self', 'file', 'schema']
_readonly_properties = ['created_at', 'updated_at', 'status', 'checksum',
'size', 'direct_url', 'self', 'file', 'schema']
_reserved_properties = ['owner', 'is_public', 'location', 'deleted',
'deleted_at']
_reserved_properties = ['owner', 'is_public', 'location', 'locations',
'deleted', 'deleted_at']
_base_properties = ['checksum', 'created_at', 'container_format',
'disk_format', 'id', 'min_disk', 'min_ram', 'name',
'size', 'status', 'tags', 'updated_at', 'visibility',
@ -431,8 +431,8 @@ class ResponseSerializer(wsgi.JSONResponseSerializer):
image_view['id'] = image.image_id
image_view['created_at'] = timeutils.isotime(image.created_at)
image_view['updated_at'] = timeutils.isotime(image.updated_at)
if CONF.show_image_direct_url and image.location is not None: # domain
image_view['direct_url'] = image.location
if CONF.show_image_direct_url and image.locations: # domain
image_view['direct_url'] = image.locations[0]
image_view['tags'] = list(image.tags)
image_view['self'] = self._get_image_href(image)
image_view['file'] = self._get_image_href(image, 'file')

View File

@ -58,7 +58,7 @@ BASE_MODEL_ATTRS = set(['id', 'created_at', 'updated_at', 'deleted_at',
IMAGE_ATTRS = BASE_MODEL_ATTRS | set(['name', 'status', 'size',
'disk_format', 'container_format',
'min_disk', 'min_ram', 'is_public',
'location', 'checksum', 'owner',
'locations', 'checksum', 'owner',
'protected'])
@ -109,7 +109,7 @@ class ImageRepo(object):
min_disk=db_image['min_disk'],
min_ram=db_image['min_ram'],
protected=db_image['protected'],
location=db_image['location'],
locations=db_image['locations'],
checksum=db_image['checksum'],
owner=db_image['owner'],
disk_format=db_image['disk_format'],
@ -128,7 +128,7 @@ class ImageRepo(object):
'min_disk': image.min_disk,
'min_ram': image.min_ram,
'protected': image.protected,
'location': image.location,
'locations': image.locations,
'checksum': image.checksum,
'owner': image.owner,
'disk_format': image.disk_format,

View File

@ -96,7 +96,7 @@ def _image_format(image_id, **values):
'id': image_id,
'name': None,
'owner': None,
'location': None,
'locations': [],
'status': 'queued',
'protected': False,
'is_public': False,
@ -349,7 +349,7 @@ def image_create(context, image_values):
raise exception.Invalid('status is a required attribute')
allowed_keys = set(['id', 'name', 'status', 'min_ram', 'min_disk', 'size',
'checksum', 'location', 'owner', 'protected',
'checksum', 'locations', 'owner', 'protected',
'is_public', 'container_format', 'disk_format',
'created_at', 'updated_at', 'deleted_at', 'deleted',
'properties', 'tags'])

View File

@ -249,11 +249,13 @@ def image_destroy(context, image_id):
"""Destroy the image or raise if it does not exist."""
session = get_session()
with session.begin():
image_ref = image_get(context, image_id, session=session)
image_ref = _image_get(context, image_id, session=session)
# Perform authorization check
check_mutate_authorization(context, image_ref)
_image_locations_set(image_ref.id, [], session)
image_ref.delete(session=session)
for prop_ref in image_ref.properties:
@ -263,20 +265,23 @@ def image_destroy(context, image_id):
for memb_ref in members:
_image_member_delete(context, memb_ref, session)
return image_ref
return _normalize_locations(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
def _normalize_locations(image):
undeleted_locations = filter(lambda x: not x.deleted, image['locations'])
image['locations'] = [loc['value'] for loc in undeleted_locations]
return image
def image_get(context, image_id, session=None, force_show_deleted=False):
image = _image_get(context, image_id, session=session,
force_show_deleted=force_show_deleted)
image = _normalize_locations(image.to_dict())
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()
@ -299,11 +304,7 @@ 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")
#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)
return image
def is_image_mutable(context, image):
@ -588,15 +589,15 @@ def image_get_all(context, filters=None, marker=None, limit=None,
marker_image = None
if marker is not None:
marker_image = image_get(context, marker,
force_show_deleted=showing_deleted)
marker_image = _image_get(context, marker,
force_show_deleted=showing_deleted)
query = paginate_query(query, models.Image, limit,
[sort_key, 'created_at', 'id'],
marker=marker_image,
sort_dir=sort_dir)
return [_limit_image_locations(image) for image in query.all()]
return [_normalize_locations(image.to_dict()) for image in query.all()]
def _drop_protected_attrs(model_class, values):
@ -656,13 +657,13 @@ def _image_update(context, values, image_id, purge_props=False):
properties = values.pop('properties', {})
try:
location = values.pop('location')
location_provided = True
locations = values.pop('locations')
locations_provided = True
except KeyError:
location_provided = False
locations_provided = False
if image_id:
image_ref = image_get(context, image_id, session=session)
image_ref = _image_get(context, image_id, session=session)
# Perform authorization check
check_mutate_authorization(context, image_ref)
@ -708,32 +709,21 @@ 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)
if locations_provided:
_image_locations_set(image_ref.id, locations, 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:
def _image_locations_set(image_id, locations, session):
location_refs = session.query(models.ImageLocation)\
.filter_by(image_id=image_id)\
.filter_by(deleted=False)\
.all()
for location_ref in location_refs:
location_ref.delete(session=session)
if location is not None:
for location in locations:
location_ref = models.ImageLocation(image_id=image_id, value=location)
location_ref.save()

View File

@ -21,7 +21,7 @@ from glance.openstack.common import uuidutils
class ImageFactory(object):
_readonly_properties = ['created_at', 'updated_at', 'status', 'checksum',
'size']
_reserved_properties = ['owner', 'is_public', 'location',
_reserved_properties = ['owner', 'is_public', 'locations',
'deleted', 'deleted_at', 'direct_url', 'self',
'file', 'schema']
@ -76,7 +76,7 @@ class Image(object):
self.min_disk = kwargs.pop('min_disk', 0)
self.min_ram = kwargs.pop('min_ram', 0)
self.protected = kwargs.pop('protected', False)
self.location = kwargs.pop('location', None)
self.locations = kwargs.pop('locations', [])
self.checksum = kwargs.pop('checksum', None)
self.owner = kwargs.pop('owner', None)
self.disk_format = kwargs.pop('disk_format', None)
@ -166,7 +166,7 @@ class ImageProxy(object):
min_disk = _proxy('base', 'min_disk')
min_ram = _proxy('base', 'min_ram')
protected = _proxy('base', 'protected')
location = _proxy('base', 'location')
locations = _proxy('base', 'locations')
checksum = _proxy('base', 'checksum')
owner = _proxy('base', 'owner')
disk_format = _proxy('base', 'disk_format')

View File

@ -351,6 +351,9 @@ class Controller(object):
msg = _("Invalid image id format")
return exc.HTTPBadRequest(explanation=msg)
if 'location' in image_data:
image_data['locations'] = [image_data.pop('location')]
try:
image_data = self.db_api.image_create(req.context, image_data)
msg = _("Successfully created image %(id)s")
@ -383,6 +386,9 @@ class Controller(object):
if not req.context.is_admin and 'owner' in image_data:
del image_data['owner']
if 'location' in image_data:
image_data['locations'] = [image_data.pop('location')]
purge_props = req.headers.get("X-Glance-Registry-Purge-Props", "false")
try:
LOG.debug(_("Updating image %(id)s with metadata: "
@ -421,6 +427,14 @@ class Controller(object):
content_type='text/plain')
def _limit_locations(image):
locations = image.pop('locations', [])
try:
image['location'] = locations[0]
except IndexError:
image['location'] = None
def make_image_dict(image):
"""
Create a dict representation of an image which we can use to
@ -438,8 +452,9 @@ def make_image_dict(image):
for p in image['properties'] if not p['deleted'])
image_dict = _fetch_attrs(image, glance.db.IMAGE_ATTRS)
image_dict['properties'] = properties
_limit_locations(image_dict)
return image_dict

View File

@ -332,15 +332,16 @@ class ImageProxy(glance.domain.ImageProxy):
def delete(self):
self.image.delete()
if self.image.location:
if self.image.locations:
if CONF.delayed_delete:
self.image.status = 'pending_delete'
self.store_api.schedule_delayed_delete_from_backend(
self.image.location, self.image.image_id)
for location in self.image.locations:
self.store_api.schedule_delayed_delete_from_backend(
location, self.image.image_id)
else:
self.store_api.safe_delete_from_backend(self.image.location,
self.context,
self.image.image_id)
for location in self.image.locations:
self.store_api.safe_delete_from_backend(
location, self.context, self.image.image_id)
def set_data(self, data, size=None):
if size is None:
@ -348,14 +349,14 @@ class ImageProxy(glance.domain.ImageProxy):
location, size, checksum = self.store_api.add_to_backend(
self.context, CONF.default_store,
self.image.image_id, data, size)
self.image.location = location
self.image.locations = [location]
self.image.size = size
self.image.checksum = checksum
self.image.status = 'active'
def get_data(self):
if not self.image.location:
if not self.image.locations:
raise exception.NotFound(_("No image data could be found"))
data, size = self.store_api.get_from_backend(self.context,
self.image.location)
self.image.locations[0])
return data

View File

@ -53,7 +53,7 @@ def build_image_fixture(**kwargs):
'min_disk': 5,
'min_ram': 256,
'size': 19,
'location': "file:///tmp/glance-tests/2",
'locations': ["file:///tmp/glance-tests/2"],
'properties': {},
}
image.update(kwargs)
@ -125,7 +125,7 @@ class DriverTests(object):
self.assertEqual(None, image['size'])
self.assertEqual(None, image['checksum'])
self.assertEqual(None, image['disk_format'])
self.assertEqual(None, image['location'])
self.assertEqual([], image['locations'])
self.assertEqual(False, image['protected'])
self.assertEqual(False, image['deleted'])
self.assertEqual(None, image['deleted_at'])
@ -145,6 +145,11 @@ class DriverTests(object):
self.db_api.image_create,
self.context, {'id': UUID1, 'status': 'queued'})
def test_image_create_with_locations(self):
fixture = {'status': 'queued', 'locations': ['a', 'b']}
image = self.db_api.image_create(self.context, fixture)
self.assertEqual(['a', 'b'], image['locations'])
def test_image_create_properties(self):
fixture = {'status': 'queued', 'properties': {'ping': 'pong'}}
image = self.db_api.image_create(self.context, fixture)
@ -164,6 +169,11 @@ class DriverTests(object):
self.assertEqual('queued', image['status'])
self.assertNotEqual(image['created_at'], image['updated_at'])
def test_image_update_with_locations(self):
fixture = {'locations': ['a', 'b']}
image = self.db_api.image_update(self.adm_context, UUID3, fixture)
self.assertEqual(['a', 'b'], image['locations'])
def test_image_update(self):
fixture = {'status': 'queued', 'properties': {'ping': 'pong'}}
image = self.db_api.image_update(self.adm_context, UUID3, fixture)
@ -411,6 +421,13 @@ class DriverTests(object):
images = self.db_api.image_get_all(self.context, limit=2)
self.assertEquals(2, len(images))
def test_image_destroy(self):
image = self.db_api.image_destroy(self.adm_context, UUID3)
self.assertTrue(image['deleted'])
self.assertTrue(image['deleted_at'])
self.assertRaises(exception.NotFound, self.db_api.image_get,
self.context, UUID3)
def test_image_get_multiple_members(self):
TENANT1 = uuidutils.generate_uuid()
TENANT2 = uuidutils.generate_uuid()

View File

@ -639,8 +639,8 @@ class TestImmutableImage(utils.BaseTestCase):
def test_change_updated_at(self):
self._test_change('updated_at', timeutils.utcnow())
def test_change_location(self):
self._test_change('location', 'http://a/b/c')
def test_change_locations(self):
self._test_change('locations', ['http://a/b/c'])
def test_change_size(self):
self._test_change('size', 32)

View File

@ -45,7 +45,7 @@ def _db_fixture(id, **kwargs):
'status': 'queued',
'tags': [],
'size': None,
'location': None,
'locations': [],
'protected': False,
'disk_format': None,
'container_format': None,

View File

@ -449,7 +449,7 @@ class TestImageNotifications(utils.BaseTestCase):
visibility='public', container_format='ami',
tags=['one', 'two'], disk_format='ami', min_ram=128,
min_disk=10, checksum='ca425b88f047ce8ec45ee90e813ada91',
location='http://127.0.0.1')
locations=['http://127.0.0.1'])
self.context = glance.context.RequestContext(tenant=TENANT2,
user=USER1)
self.image_repo_stub = ImageRepoStub()

View File

@ -34,10 +34,10 @@ class ImageRepoStub(object):
class ImageStub(object):
def __init__(self, image_id, status, location):
def __init__(self, image_id, status, locations):
self.image_id = image_id
self.status = status
self.location = location
self.locations = locations
def delete(self):
self.status = 'deleted'
@ -45,20 +45,21 @@ class ImageStub(object):
class TestStoreImage(utils.BaseTestCase):
def setUp(self):
location = '%s/%s' % (BASE_URI, UUID1)
self.image_stub = ImageStub(UUID1, 'active', location)
locations = ['%s/%s' % (BASE_URI, UUID1)]
self.image_stub = ImageStub(UUID1, 'active', locations)
self.image_repo_stub = ImageRepoStub()
self.store_api = unit_test_utils.FakeStoreAPI()
super(TestStoreImage, self).setUp()
def test_image_delete(self):
image = glance.store.ImageProxy(self.image_stub, {}, self.store_api)
location = image.locations[0]
self.assertEquals(image.status, 'active')
self.store_api.get_from_backend({}, image.location) # no exception
self.store_api.get_from_backend({}, location)
image.delete()
self.assertEquals(image.status, 'deleted')
self.assertRaises(exception.NotFound,
self.store_api.get_from_backend, {}, image.location)
self.store_api.get_from_backend, {}, location)
def test_image_delayed_delete(self):
self.config(delayed_delete=True)
@ -66,7 +67,7 @@ class TestStoreImage(utils.BaseTestCase):
self.assertEquals(image.status, 'active')
image.delete()
self.assertEquals(image.status, 'pending_delete')
self.store_api.get_from_backend({}, image.location) # no exception
self.store_api.get_from_backend({}, image.locations[0]) # no exception
def test_image_get_data(self):
image = glance.store.ImageProxy(self.image_stub, {}, self.store_api)
@ -74,23 +75,23 @@ class TestStoreImage(utils.BaseTestCase):
def test_image_set_data(self):
context = glance.context.RequestContext(user=USER1)
image_stub = ImageStub(UUID2, status='queued', location=None)
image_stub = ImageStub(UUID2, status='queued', locations=[])
image = glance.store.ImageProxy(image_stub, context, self.store_api)
image.set_data('YYYY', 4)
self.assertEquals(image.size, 4)
#NOTE(markwash): FakeStore returns image_id for location
self.assertEquals(image.location, UUID2)
self.assertEquals(image.locations, [UUID2])
self.assertEquals(image.checksum, 'Z')
self.assertEquals(image.status, 'active')
def test_image_set_data_unknown_size(self):
context = glance.context.RequestContext(user=USER1)
image_stub = ImageStub(UUID2, status='queued', location=None)
image_stub = ImageStub(UUID2, status='queued', locations=[])
image = glance.store.ImageProxy(image_stub, context, self.store_api)
image.set_data('YYYY', None)
self.assertEquals(image.size, 4)
#NOTE(markwash): FakeStore returns image_id for location
self.assertEquals(image.location, UUID2)
self.assertEquals(image.locations, [UUID2])
self.assertEquals(image.checksum, 'Z')
self.assertEquals(image.status, 'active')

View File

@ -61,7 +61,7 @@ class FakeDB(object):
def init_db():
images = [
{'id': UUID1, 'owner': TENANT1, 'status': 'queued',
'location': '%s/%s' % (BASE_URI, UUID1)},
'locations': ['%s/%s' % (BASE_URI, UUID1)]},
{'id': UUID2, 'owner': TENANT1, 'status': 'queued'},
]
[simple_db.image_create(None, image) for image in images]

View File

@ -125,7 +125,7 @@ class TestRegistryAPI(base.IsolatedUnitTest):
'min_disk': 0,
'min_ram': 0,
'size': 13,
'location': "file:///%s/%s" % (self.test_dir, UUID1),
'locations': ["file:///%s/%s" % (self.test_dir, UUID1)],
'properties': {'type': 'kernel'}},
{'id': UUID2,
'name': 'fake image #2',
@ -141,7 +141,7 @@ class TestRegistryAPI(base.IsolatedUnitTest):
'min_disk': 5,
'min_ram': 256,
'size': 19,
'location': "file:///%s/%s" % (self.test_dir, UUID2),
'locations': ["file:///%s/%s" % (self.test_dir, UUID2)],
'properties': {}}]
self.context = glance.context.RequestContext(is_admin=True)
db_api.configure_db()
@ -1943,7 +1943,7 @@ class TestGlanceAPI(base.IsolatedUnitTest):
'deleted': False,
'checksum': None,
'size': 13,
'location': "file:///%s/%s" % (self.test_dir, UUID1),
'locations': ["file:///%s/%s" % (self.test_dir, UUID1)],
'properties': {'type': 'kernel'}},
{'id': UUID2,
'name': 'fake image #2',
@ -1957,7 +1957,7 @@ class TestGlanceAPI(base.IsolatedUnitTest):
'deleted': False,
'checksum': None,
'size': 19,
'location': "file:///%s/%s" % (self.test_dir, UUID2),
'locations': ["file:///%s/%s" % (self.test_dir, UUID2)],
'properties': {}}]
self.context = glance.context.RequestContext(is_admin=True)
db_api.configure_db()

View File

@ -35,12 +35,12 @@ class Raise(object):
class FakeImage(object):
def __init__(self, image_id=None, data=None, checksum=None, size=0,
location=None):
locations=None):
self.image_id = image_id
self.data = data
self.checksum = checksum
self.size = size
self.location = location
self.locations = locations
def get_data(self):
return self.data
@ -84,7 +84,7 @@ class TestImagesController(base.StoreClearingUnitTest):
def test_download(self):
request = unit_test_utils.get_fake_request()
image = FakeImage('abcd', location='http://example.com/image')
image = FakeImage('abcd', locations=['http://example.com/image'])
self.image_repo.result = image
image = self.controller.download(request, unit_test_utils.UUID1)
self.assertEqual(image.image_id, 'abcd')

View File

@ -56,7 +56,7 @@ def _db_fixture(id, **kwargs):
'status': 'queued',
'tags': [],
'size': None,
'location': None,
'locations': [],
'protected': False,
'disk_format': None,
'container_format': None,
@ -106,7 +106,8 @@ class TestImageMembersController(test_utils.BaseTestCase):
self.db.reset()
self.images = [
_db_fixture(UUID1, owner=TENANT1, name='1', size=256,
is_public=True, location='%s/%s' % (BASE_URI, UUID1)),
is_public=True,
locations=['%s/%s' % (BASE_URI, UUID1)]),
_db_fixture(UUID2, owner=TENANT1, name='2', size=512),
_db_fixture(UUID3, owner=TENANT3, name='3', size=512),
_db_fixture(UUID4, owner=TENANT4, name='4', size=1024),

View File

@ -59,7 +59,7 @@ def _db_fixture(id, **kwargs):
'status': 'queued',
'tags': [],
'size': None,
'location': None,
'locations': [],
'protected': False,
'disk_format': None,
'container_format': None,
@ -80,7 +80,7 @@ def _domain_fixture(id, **kwargs):
'owner': None,
'status': 'queued',
'size': None,
'location': None,
'locations': [],
'protected': False,
'disk_format': None,
'container_format': None,
@ -121,7 +121,8 @@ class TestImagesController(test_utils.BaseTestCase):
self.db.reset()
self.images = [
_db_fixture(UUID1, owner=TENANT1, name='1', size=256,
is_public=True, location='%s/%s' % (BASE_URI, UUID1)),
is_public=True,
locations=['%s/%s' % (BASE_URI, UUID1)]),
_db_fixture(UUID2, owner=TENANT1, name='2',
size=512, is_public=True),
_db_fixture(UUID3, owner=TENANT3, name='3',
@ -1046,7 +1047,7 @@ class TestImagesDeserializer(test_utils.BaseTestCase):
samples = {
'owner': TENANT1,
'is_public': True,
'location': '/a/b/c/d',
'locations': ['/a/b/c/d'],
'deleted': False,
'deleted_at': ISOTIME,
}
@ -1876,7 +1877,7 @@ class TestImagesSerializerDirectUrl(test_utils.BaseTestCase):
self.active_image = _domain_fixture(
UUID1, name='image-1', visibility='public',
status='active', size=1024, created_at=DATETIME,
updated_at=DATETIME, location='http://some/fake/location')
updated_at=DATETIME, locations=['http://some/fake/location'])
self.queued_image = _domain_fixture(
UUID2, name='image-2', status='active',