Filter images by status and add visibility shared
This patch allows the user to filter images by status. Whenever a user does an image list, the following are displayed: (1) public images (2) images owned by the user (3) images whom the user is a member of and has set status to accepted The user can change the status in (3) and also choose to display the images the user has been requested to be a member of, rejected to be a member of, or all images irrespective of the status set by user If visibility filter can be shared, public or private. Where shared filter displays images which the user is a member of. This can be used in conjunction with member_status Related to bp glance-api-v2-image-sharing Change-Id: I052c0947f9ad17ef1b14af0ad0f51b886afd0d7a
This commit is contained in:
parent
d706143781
commit
ac1c9ace1e
|
@ -65,7 +65,7 @@ class ImagesController(object):
|
||||||
return image
|
return image
|
||||||
|
|
||||||
def index(self, req, marker=None, limit=None, sort_key='created_at',
|
def index(self, req, marker=None, limit=None, sort_key='created_at',
|
||||||
sort_dir='desc', filters=None):
|
sort_dir='desc', filters=None, member_status='accepted'):
|
||||||
result = {}
|
result = {}
|
||||||
if filters is None:
|
if filters is None:
|
||||||
filters = {}
|
filters = {}
|
||||||
|
@ -79,7 +79,8 @@ class ImagesController(object):
|
||||||
try:
|
try:
|
||||||
images = image_repo.list(marker=marker, limit=limit,
|
images = image_repo.list(marker=marker, limit=limit,
|
||||||
sort_key=sort_key, sort_dir=sort_dir,
|
sort_key=sort_key, sort_dir=sort_dir,
|
||||||
filters=filters)
|
filters=filters,
|
||||||
|
member_status=member_status)
|
||||||
if len(images) != 0 and len(images) == limit:
|
if len(images) != 0 and len(images) == limit:
|
||||||
result['next_marker'] = images[-1].image_id
|
result['next_marker'] = images[-1].image_id
|
||||||
except (exception.NotFound, exception.InvalidSortKey,
|
except (exception.NotFound, exception.InvalidSortKey,
|
||||||
|
@ -331,12 +332,17 @@ class RequestDeserializer(wsgi.JSONRequestDeserializer):
|
||||||
|
|
||||||
return sort_dir
|
return sort_dir
|
||||||
|
|
||||||
|
def _validate_member_status(self, member_status):
|
||||||
|
if member_status not in ['pending', 'accepted', 'rejected', 'all']:
|
||||||
|
msg = _('Invalid status: %s' % member_status)
|
||||||
|
raise webob.exc.HTTPBadRequest(explanation=msg)
|
||||||
|
|
||||||
|
return member_status
|
||||||
|
|
||||||
def _get_filters(self, filters):
|
def _get_filters(self, filters):
|
||||||
visibility = filters.pop('visibility', None)
|
visibility = filters.get('visibility', None)
|
||||||
if visibility:
|
if visibility:
|
||||||
if visibility in ['public', 'private']:
|
if visibility not in ['public', 'private', 'shared']:
|
||||||
filters['is_public'] = visibility == 'public'
|
|
||||||
else:
|
|
||||||
msg = _('Invalid visibility value: %s') % visibility
|
msg = _('Invalid visibility value: %s') % visibility
|
||||||
raise webob.exc.HTTPBadRequest(explanation=msg)
|
raise webob.exc.HTTPBadRequest(explanation=msg)
|
||||||
|
|
||||||
|
@ -347,10 +353,12 @@ class RequestDeserializer(wsgi.JSONRequestDeserializer):
|
||||||
limit = params.pop('limit', None)
|
limit = params.pop('limit', None)
|
||||||
marker = params.pop('marker', None)
|
marker = params.pop('marker', None)
|
||||||
sort_dir = params.pop('sort_dir', 'desc')
|
sort_dir = params.pop('sort_dir', 'desc')
|
||||||
|
member_status = params.pop('member_status', 'accepted')
|
||||||
query_params = {
|
query_params = {
|
||||||
'sort_key': params.pop('sort_key', 'created_at'),
|
'sort_key': params.pop('sort_key', 'created_at'),
|
||||||
'sort_dir': self._validate_sort_dir(sort_dir),
|
'sort_dir': self._validate_sort_dir(sort_dir),
|
||||||
'filters': self._get_filters(params),
|
'filters': self._get_filters(params),
|
||||||
|
'member_status': self._validate_member_status(member_status),
|
||||||
}
|
}
|
||||||
|
|
||||||
if marker is not None:
|
if marker is not None:
|
||||||
|
|
|
@ -79,11 +79,11 @@ class ImageRepo(object):
|
||||||
return ImageProxy(image, self.context, self.db_api)
|
return ImageProxy(image, self.context, self.db_api)
|
||||||
|
|
||||||
def list(self, marker=None, limit=None, sort_key='created_at',
|
def list(self, marker=None, limit=None, sort_key='created_at',
|
||||||
sort_dir='desc', filters=None):
|
sort_dir='desc', filters=None, member_status='accepted'):
|
||||||
db_filters = self._translate_filters(filters)
|
|
||||||
db_api_images = self.db_api.image_get_all(
|
db_api_images = self.db_api.image_get_all(
|
||||||
self.context, filters=db_filters, marker=marker, limit=limit,
|
self.context, filters=filters, marker=marker, limit=limit,
|
||||||
sort_key=sort_key, sort_dir=sort_dir)
|
sort_key=sort_key, sort_dir=sort_dir,
|
||||||
|
member_status=member_status)
|
||||||
images = []
|
images = []
|
||||||
for db_api_image in db_api_images:
|
for db_api_image in db_api_images:
|
||||||
tags = self.db_api.image_tag_get_all(self.context,
|
tags = self.db_api.image_tag_get_all(self.context,
|
||||||
|
@ -92,17 +92,6 @@ class ImageRepo(object):
|
||||||
images.append(image)
|
images.append(image)
|
||||||
return images
|
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):
|
def _format_image_from_db(self, db_image, db_tags):
|
||||||
visibility = 'public' if db_image['is_public'] else 'private'
|
visibility = 'public' if db_image['is_public'] else 'private'
|
||||||
properties = {}
|
properties = {}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
# Copyright 2012 OpenStack Foundation
|
# Copyright 2012 OpenStack, Foundation
|
||||||
# All Rights Reserved.
|
# All Rights Reserved.
|
||||||
#
|
#
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||||
|
@ -124,7 +124,7 @@ def _image_format(image_id, **values):
|
||||||
return image
|
return image
|
||||||
|
|
||||||
|
|
||||||
def _filter_images(images, filters, context):
|
def _filter_images(images, filters, context, status='accepted'):
|
||||||
filtered_images = []
|
filtered_images = []
|
||||||
if 'properties' in filters:
|
if 'properties' in filters:
|
||||||
prop_filter = filters.pop('properties')
|
prop_filter = filters.pop('properties')
|
||||||
|
@ -133,12 +133,33 @@ def _filter_images(images, filters, context):
|
||||||
if 'is_public' in filters and filters['is_public'] is None:
|
if 'is_public' in filters and filters['is_public'] is None:
|
||||||
filters.pop('is_public')
|
filters.pop('is_public')
|
||||||
|
|
||||||
|
if status == 'all':
|
||||||
|
status = None
|
||||||
|
|
||||||
|
visibility = filters.pop('visibility', None)
|
||||||
|
|
||||||
for image in images:
|
for image in images:
|
||||||
|
member = image_member_find(context, image_id=image['id'],
|
||||||
|
member=context.owner, status=status)
|
||||||
|
is_member = len(member) > 0
|
||||||
has_ownership = context.owner and image['owner'] == context.owner
|
has_ownership = context.owner and image['owner'] == context.owner
|
||||||
can_see = image['is_public'] or has_ownership or context.is_admin
|
can_see = (image['is_public'] or has_ownership or context.is_admin or
|
||||||
|
is_member)
|
||||||
if not can_see:
|
if not can_see:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
if visibility:
|
||||||
|
if visibility == 'public':
|
||||||
|
filters['is_public'] = True
|
||||||
|
if not image['is_public']:
|
||||||
|
continue
|
||||||
|
elif visibility == 'private':
|
||||||
|
filters['is_public'] = False
|
||||||
|
if not (has_ownership or context.is_admin):
|
||||||
|
continue
|
||||||
|
elif visibility == 'shared':
|
||||||
|
if not is_member:
|
||||||
|
continue
|
||||||
add = True
|
add = True
|
||||||
for k, value in filters.iteritems():
|
for k, value in filters.iteritems():
|
||||||
key = k
|
key = k
|
||||||
|
@ -172,14 +193,16 @@ def _filter_images(images, filters, context):
|
||||||
return filtered_images
|
return filtered_images
|
||||||
|
|
||||||
|
|
||||||
def _do_pagination(context, images, marker, limit, show_deleted):
|
def _do_pagination(context, images, marker, limit, show_deleted,
|
||||||
|
status='accepted'):
|
||||||
start = 0
|
start = 0
|
||||||
end = -1
|
end = -1
|
||||||
if marker is None:
|
if marker is None:
|
||||||
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,
|
||||||
|
status=status)
|
||||||
|
|
||||||
for i, image in enumerate(images):
|
for i, image in enumerate(images):
|
||||||
if image['id'] == marker:
|
if image['id'] == marker:
|
||||||
|
@ -203,7 +226,7 @@ def _sort_images(images, sort_key, sort_dir):
|
||||||
return images
|
return images
|
||||||
|
|
||||||
|
|
||||||
def _image_get(context, image_id, force_show_deleted=False):
|
def _image_get(context, image_id, force_show_deleted=False, status=None):
|
||||||
try:
|
try:
|
||||||
image = DATA['images'][image_id]
|
image = DATA['images'][image_id]
|
||||||
except KeyError:
|
except KeyError:
|
||||||
|
@ -229,10 +252,11 @@ def image_get(context, image_id, session=None, force_show_deleted=False):
|
||||||
|
|
||||||
@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',
|
||||||
|
member_status='accepted'):
|
||||||
filters = filters or {}
|
filters = filters or {}
|
||||||
images = DATA['images'].values()
|
images = DATA['images'].values()
|
||||||
images = _filter_images(images, filters, context)
|
images = _filter_images(images, filters, context, member_status)
|
||||||
images = _sort_images(images, sort_key, sort_dir)
|
images = _sort_images(images, sort_key, sort_dir)
|
||||||
images = _do_pagination(context, images, marker, limit,
|
images = _do_pagination(context, images, marker, limit,
|
||||||
filters.get('deleted'))
|
filters.get('deleted'))
|
||||||
|
@ -462,7 +486,7 @@ def is_image_sharable(context, image, **kwargs):
|
||||||
return member['can_share']
|
return member['can_share']
|
||||||
|
|
||||||
|
|
||||||
def is_image_visible(context, image):
|
def is_image_visible(context, image, status=None):
|
||||||
"""Return True if the image is visible in this context."""
|
"""Return True if the image is visible in this context."""
|
||||||
# Is admin == image visible
|
# Is admin == image visible
|
||||||
if context.is_admin:
|
if context.is_admin:
|
||||||
|
@ -482,10 +506,13 @@ def is_image_visible(context, image):
|
||||||
return True
|
return True
|
||||||
|
|
||||||
# Figure out if this image is shared with that tenant
|
# Figure out if this image is shared with that tenant
|
||||||
|
if status == 'all':
|
||||||
|
status = None
|
||||||
members = image_member_find(context,
|
members = image_member_find(context,
|
||||||
image_id=image['id'],
|
image_id=image['id'],
|
||||||
member=context.owner)
|
member=context.owner,
|
||||||
if members:
|
status=status)
|
||||||
|
if len(members) > 0:
|
||||||
return True
|
return True
|
||||||
|
|
||||||
# Private image
|
# Private image
|
||||||
|
|
|
@ -33,9 +33,11 @@ import sqlalchemy.sql as sa_sql
|
||||||
from glance.common import exception
|
from glance.common import exception
|
||||||
from glance.db.sqlalchemy import migration
|
from glance.db.sqlalchemy import migration
|
||||||
from glance.db.sqlalchemy import models
|
from glance.db.sqlalchemy import models
|
||||||
|
from glance.openstack.common import cfg
|
||||||
import glance.openstack.common.log as os_logging
|
import glance.openstack.common.log as os_logging
|
||||||
from glance.openstack.common import timeutils
|
from glance.openstack.common import timeutils
|
||||||
|
|
||||||
|
|
||||||
_ENGINE = None
|
_ENGINE = None
|
||||||
_MAKER = None
|
_MAKER = None
|
||||||
_MAX_RETRIES = None
|
_MAX_RETRIES = None
|
||||||
|
@ -338,7 +340,7 @@ def is_image_sharable(context, image, **kwargs):
|
||||||
return member['can_share']
|
return member['can_share']
|
||||||
|
|
||||||
|
|
||||||
def is_image_visible(context, image):
|
def is_image_visible(context, image, status=None):
|
||||||
"""Return True if the image is visible in this context."""
|
"""Return True if the image is visible in this context."""
|
||||||
# Is admin == image visible
|
# Is admin == image visible
|
||||||
if context.is_admin:
|
if context.is_admin:
|
||||||
|
@ -360,7 +362,8 @@ def is_image_visible(context, image):
|
||||||
# Figure out if this image is shared with that tenant
|
# Figure out if this image is shared with that tenant
|
||||||
members = image_member_find(context,
|
members = image_member_find(context,
|
||||||
image_id=image['id'],
|
image_id=image['id'],
|
||||||
member=context.owner)
|
member=context.owner,
|
||||||
|
status=status)
|
||||||
if members:
|
if members:
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
@ -467,7 +470,8 @@ def paginate_query(query, model, limit, sort_keys, marker=None,
|
||||||
|
|
||||||
|
|
||||||
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',
|
||||||
|
member_status='accepted'):
|
||||||
"""
|
"""
|
||||||
Get all images that match zero or more filters.
|
Get all images that match zero or more filters.
|
||||||
|
|
||||||
|
@ -493,13 +497,37 @@ def image_get_all(context, filters=None, marker=None, limit=None,
|
||||||
visibility_filters = [models.Image.is_public == True]
|
visibility_filters = [models.Image.is_public == True]
|
||||||
|
|
||||||
if context.owner is not None:
|
if context.owner is not None:
|
||||||
visibility_filters.extend([
|
if member_status == 'all':
|
||||||
models.Image.owner == context.owner,
|
visibility_filters.extend([
|
||||||
models.Image.members.any(member=context.owner, deleted=False),
|
models.Image.owner == context.owner,
|
||||||
])
|
models.Image.members.any(member=context.owner,
|
||||||
|
deleted=False),
|
||||||
|
])
|
||||||
|
else:
|
||||||
|
visibility_filters.extend([
|
||||||
|
models.Image.owner == context.owner,
|
||||||
|
models.Image.members.any(member=context.owner,
|
||||||
|
deleted=False,
|
||||||
|
status=member_status),
|
||||||
|
])
|
||||||
|
|
||||||
query = query.filter(sa_sql.or_(*visibility_filters))
|
query = query.filter(sa_sql.or_(*visibility_filters))
|
||||||
|
|
||||||
|
if 'visibility' in filters:
|
||||||
|
visibility = filters.pop('visibility')
|
||||||
|
if visibility == 'public':
|
||||||
|
query = query.filter(models.Image.is_public == True)
|
||||||
|
filters['is_public'] = True
|
||||||
|
elif visibility == 'private':
|
||||||
|
filters['is_public'] = False
|
||||||
|
if (not context.is_admin) and context.owner is not None:
|
||||||
|
query = query.filter(
|
||||||
|
models.Image.owner == context.owner)
|
||||||
|
else:
|
||||||
|
query = query.filter(
|
||||||
|
models.Image.members.any(member=context.owner,
|
||||||
|
deleted=False))
|
||||||
|
|
||||||
showing_deleted = False
|
showing_deleted = False
|
||||||
if 'changes-since' in filters:
|
if 'changes-since' in filters:
|
||||||
# normalize timestamp to UTC, as sqlalchemy doesn't appear to
|
# normalize timestamp to UTC, as sqlalchemy doesn't appear to
|
||||||
|
|
|
@ -430,6 +430,37 @@ class DriverTests(object):
|
||||||
image = self.db_api.image_get(ctxt2, UUIDX)
|
image = self.db_api.image_get(ctxt2, UUIDX)
|
||||||
self.assertEquals(UUIDX, image['id'])
|
self.assertEquals(UUIDX, image['id'])
|
||||||
|
|
||||||
|
# by default get_all displays only images with status 'accepted'
|
||||||
|
images = self.db_api.image_get_all(ctxt2)
|
||||||
|
self.assertEquals(3, len(images))
|
||||||
|
|
||||||
|
# filter by rejected
|
||||||
|
images = self.db_api.image_get_all(ctxt2, member_status='rejected')
|
||||||
|
self.assertEquals(3, len(images))
|
||||||
|
|
||||||
|
# filter by visibility
|
||||||
|
images = self.db_api.image_get_all(ctxt2,
|
||||||
|
filters={'visibility': 'shared'})
|
||||||
|
self.assertEquals(0, len(images))
|
||||||
|
|
||||||
|
# filter by visibility
|
||||||
|
images = self.db_api.image_get_all(ctxt2, member_status='pending',
|
||||||
|
filters={'visibility': 'shared'})
|
||||||
|
self.assertEquals(1, len(images))
|
||||||
|
|
||||||
|
# filter by visibility
|
||||||
|
images = self.db_api.image_get_all(ctxt2, member_status='all',
|
||||||
|
filters={'visibility': 'shared'})
|
||||||
|
self.assertEquals(1, len(images))
|
||||||
|
|
||||||
|
# filter by status pending
|
||||||
|
images = self.db_api.image_get_all(ctxt2, member_status='pending')
|
||||||
|
self.assertEquals(4, len(images))
|
||||||
|
|
||||||
|
# filter by status all
|
||||||
|
images = self.db_api.image_get_all(ctxt2, member_status='all')
|
||||||
|
self.assertEquals(4, len(images))
|
||||||
|
|
||||||
def test_is_image_visible(self):
|
def test_is_image_visible(self):
|
||||||
TENANT1 = uuidutils.generate_uuid()
|
TENANT1 = uuidutils.generate_uuid()
|
||||||
TENANT2 = uuidutils.generate_uuid()
|
TENANT2 = uuidutils.generate_uuid()
|
||||||
|
|
|
@ -801,6 +801,13 @@ class TestImageMembers(functional.FunctionalTest):
|
||||||
images = json.loads(response.text)['images']
|
images = json.loads(response.text)['images']
|
||||||
self.assertEqual(4, len(images))
|
self.assertEqual(4, len(images))
|
||||||
|
|
||||||
|
# Image list should contain 3 images for TENANT3
|
||||||
|
path = self._url('/v2/images')
|
||||||
|
response = requests.get(path, headers=get_header(TENANT3))
|
||||||
|
self.assertEqual(200, response.status_code)
|
||||||
|
images = json.loads(response.text)['images']
|
||||||
|
self.assertEqual(3, len(images))
|
||||||
|
|
||||||
# Add Image member for tenant1-private image
|
# Add Image member for tenant1-private image
|
||||||
path = self._url('/v2/images/%s/members' % image_fixture[1]['id'])
|
path = self._url('/v2/images/%s/members' % image_fixture[1]['id'])
|
||||||
body = json.dumps({'member': TENANT3})
|
body = json.dumps({'member': TENANT3})
|
||||||
|
@ -814,6 +821,68 @@ class TestImageMembers(functional.FunctionalTest):
|
||||||
self.assertTrue('updated_at' in image_member)
|
self.assertTrue('updated_at' in image_member)
|
||||||
self.assertEqual('pending', image_member['status'])
|
self.assertEqual('pending', image_member['status'])
|
||||||
|
|
||||||
|
# Image list should contain 3 images for TENANT3
|
||||||
|
path = self._url('/v2/images')
|
||||||
|
response = requests.get(path, headers=get_header(TENANT3))
|
||||||
|
self.assertEqual(200, response.status_code)
|
||||||
|
images = json.loads(response.text)['images']
|
||||||
|
self.assertEqual(3, len(images))
|
||||||
|
|
||||||
|
# Image list should contain 0 shared images for TENANT3
|
||||||
|
# becuase default is accepted
|
||||||
|
path = self._url('/v2/images?visibility=shared')
|
||||||
|
response = requests.get(path, headers=get_header(TENANT3))
|
||||||
|
self.assertEqual(200, response.status_code)
|
||||||
|
images = json.loads(response.text)['images']
|
||||||
|
self.assertEqual(0, len(images))
|
||||||
|
|
||||||
|
# Image list should contain 4 images for TENANT3 with status pending
|
||||||
|
path = self._url('/v2/images?member_status=pending')
|
||||||
|
response = requests.get(path, headers=get_header(TENANT3))
|
||||||
|
self.assertEqual(200, response.status_code)
|
||||||
|
images = json.loads(response.text)['images']
|
||||||
|
self.assertEqual(4, len(images))
|
||||||
|
|
||||||
|
# Image list should contain 4 images for TENANT3 with status all
|
||||||
|
path = self._url('/v2/images?member_status=all')
|
||||||
|
response = requests.get(path, headers=get_header(TENANT3))
|
||||||
|
self.assertEqual(200, response.status_code)
|
||||||
|
images = json.loads(response.text)['images']
|
||||||
|
self.assertEqual(4, len(images))
|
||||||
|
|
||||||
|
# Image list should contain 1 image for TENANT3 with status pending
|
||||||
|
# and visibility shared
|
||||||
|
path = self._url('/v2/images?member_status=pending&visibility=shared')
|
||||||
|
response = requests.get(path, headers=get_header(TENANT3))
|
||||||
|
self.assertEqual(200, response.status_code)
|
||||||
|
images = json.loads(response.text)['images']
|
||||||
|
self.assertEqual(1, len(images))
|
||||||
|
self.assertEqual(images[0]['name'], 'tenant1-private')
|
||||||
|
|
||||||
|
# Image list should contain 0 image for TENANT3 with status rejected
|
||||||
|
# and visibility shared
|
||||||
|
path = self._url('/v2/images?member_status=rejected&visibility=shared')
|
||||||
|
response = requests.get(path, headers=get_header(TENANT3))
|
||||||
|
self.assertEqual(200, response.status_code)
|
||||||
|
images = json.loads(response.text)['images']
|
||||||
|
self.assertEqual(0, len(images))
|
||||||
|
|
||||||
|
# Image list should contain 0 image for TENANT3 with status accepted
|
||||||
|
# and visibility shared
|
||||||
|
path = self._url('/v2/images?member_status=accepted&visibility=shared')
|
||||||
|
response = requests.get(path, headers=get_header(TENANT3))
|
||||||
|
self.assertEqual(200, response.status_code)
|
||||||
|
images = json.loads(response.text)['images']
|
||||||
|
self.assertEqual(0, len(images))
|
||||||
|
|
||||||
|
# Image list should contain 0 image for TENANT3 with status accepted
|
||||||
|
# and visibility private
|
||||||
|
path = self._url('/v2/images?visibility=private')
|
||||||
|
response = requests.get(path, headers=get_header(TENANT3))
|
||||||
|
self.assertEqual(200, response.status_code)
|
||||||
|
images = json.loads(response.text)['images']
|
||||||
|
self.assertEqual(0, len(images))
|
||||||
|
|
||||||
# Image tenant2-private's image members list should contain no members
|
# Image tenant2-private's image members list should contain no members
|
||||||
path = self._url('/v2/images/%s/members' % image_fixture[3]['id'])
|
path = self._url('/v2/images/%s/members' % image_fixture[3]['id'])
|
||||||
response = requests.get(path, headers=get_header('tenant2'))
|
response = requests.get(path, headers=get_header('tenant2'))
|
||||||
|
@ -828,13 +897,6 @@ class TestImageMembers(functional.FunctionalTest):
|
||||||
response = requests.put(path, headers=get_header('tenant1'), data=body)
|
response = requests.put(path, headers=get_header('tenant1'), data=body)
|
||||||
self.assertEqual(403, response.status_code)
|
self.assertEqual(403, response.status_code)
|
||||||
|
|
||||||
# Image list should contain 4 images for TENANT3
|
|
||||||
path = self._url('/v2/images')
|
|
||||||
response = requests.get(path, headers=get_header(TENANT3))
|
|
||||||
self.assertEqual(200, response.status_code)
|
|
||||||
images = json.loads(response.text)['images']
|
|
||||||
self.assertEqual(4, len(images))
|
|
||||||
|
|
||||||
# Tenant 3 can change status of image
|
# Tenant 3 can change status of image
|
||||||
path = self._url('/v2/images/%s/members/%s' % (image_fixture[1]['id'],
|
path = self._url('/v2/images/%s/members/%s' % (image_fixture[1]['id'],
|
||||||
TENANT3))
|
TENANT3))
|
||||||
|
|
|
@ -76,6 +76,7 @@ class TestImageRepo(test_utils.BaseTestCase):
|
||||||
self.image_repo = glance.db.ImageRepo(self.context, self.db)
|
self.image_repo = glance.db.ImageRepo(self.context, self.db)
|
||||||
self.image_factory = glance.domain.ImageFactory()
|
self.image_factory = glance.domain.ImageFactory()
|
||||||
self._create_images()
|
self._create_images()
|
||||||
|
self._create_image_members()
|
||||||
super(TestImageRepo, self).setUp()
|
super(TestImageRepo, self).setUp()
|
||||||
|
|
||||||
def _create_images(self):
|
def _create_images(self):
|
||||||
|
@ -93,6 +94,14 @@ class TestImageRepo(test_utils.BaseTestCase):
|
||||||
|
|
||||||
self.db.image_tag_set_all(None, UUID1, ['ping', 'pong'])
|
self.db.image_tag_set_all(None, UUID1, ['ping', 'pong'])
|
||||||
|
|
||||||
|
def _create_image_members(self):
|
||||||
|
self.image_members = [
|
||||||
|
_db_image_member_fixture(UUID2, TENANT2),
|
||||||
|
_db_image_member_fixture(UUID2, TENANT3, status='accepted'),
|
||||||
|
]
|
||||||
|
[self.db.image_member_create(None, image_member)
|
||||||
|
for image_member in self.image_members]
|
||||||
|
|
||||||
def test_get(self):
|
def test_get(self):
|
||||||
image = self.image_repo.get(UUID1)
|
image = self.image_repo.get(UUID1)
|
||||||
self.assertEquals(image.image_id, UUID1)
|
self.assertEquals(image.image_id, UUID1)
|
||||||
|
@ -115,6 +124,25 @@ class TestImageRepo(test_utils.BaseTestCase):
|
||||||
image_ids = set([i.image_id for i in images])
|
image_ids = set([i.image_id for i in images])
|
||||||
self.assertEqual(set([UUID1, UUID2, UUID3]), image_ids)
|
self.assertEqual(set([UUID1, UUID2, UUID3]), image_ids)
|
||||||
|
|
||||||
|
def _do_test_list_status(self, status, expected):
|
||||||
|
self.context = glance.context.RequestContext(
|
||||||
|
user=USER1, tenant=TENANT3)
|
||||||
|
self.image_repo = glance.db.ImageRepo(self.context, self.db)
|
||||||
|
images = self.image_repo.list(member_status=status)
|
||||||
|
self.assertEqual(expected, len(images))
|
||||||
|
|
||||||
|
def test_list_status(self):
|
||||||
|
self._do_test_list_status(None, 3)
|
||||||
|
|
||||||
|
def test_list_status_pending(self):
|
||||||
|
self._do_test_list_status('pending', 2)
|
||||||
|
|
||||||
|
def test_list_status_rejected(self):
|
||||||
|
self._do_test_list_status('rejected', 2)
|
||||||
|
|
||||||
|
def test_list_status_all(self):
|
||||||
|
self._do_test_list_status('all', 3)
|
||||||
|
|
||||||
def test_list_with_marker(self):
|
def test_list_with_marker(self):
|
||||||
full_images = self.image_repo.list()
|
full_images = self.image_repo.list()
|
||||||
full_ids = [i.image_id for i in full_images]
|
full_ids = [i.image_id for i in full_images]
|
||||||
|
|
|
@ -92,6 +92,15 @@ def _domain_fixture(id, **kwargs):
|
||||||
return glance.domain.Image(**properties)
|
return glance.domain.Image(**properties)
|
||||||
|
|
||||||
|
|
||||||
|
def _db_image_member_fixture(image_id, member_id, **kwargs):
|
||||||
|
obj = {
|
||||||
|
'image_id': image_id,
|
||||||
|
'member': member_id,
|
||||||
|
}
|
||||||
|
obj.update(kwargs)
|
||||||
|
return obj
|
||||||
|
|
||||||
|
|
||||||
class TestImagesController(test_utils.BaseTestCase):
|
class TestImagesController(test_utils.BaseTestCase):
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
|
@ -101,6 +110,7 @@ class TestImagesController(test_utils.BaseTestCase):
|
||||||
self.notifier = unit_test_utils.FakeNotifier()
|
self.notifier = unit_test_utils.FakeNotifier()
|
||||||
self.store = unit_test_utils.FakeStoreAPI()
|
self.store = unit_test_utils.FakeStoreAPI()
|
||||||
self._create_images()
|
self._create_images()
|
||||||
|
self._create_image_members()
|
||||||
self.controller = glance.api.v2.images.ImagesController(self.db,
|
self.controller = glance.api.v2.images.ImagesController(self.db,
|
||||||
self.policy,
|
self.policy,
|
||||||
self.notifier,
|
self.notifier,
|
||||||
|
@ -122,6 +132,15 @@ class TestImagesController(test_utils.BaseTestCase):
|
||||||
|
|
||||||
self.db.image_tag_set_all(None, UUID1, ['ping', 'pong'])
|
self.db.image_tag_set_all(None, UUID1, ['ping', 'pong'])
|
||||||
|
|
||||||
|
def _create_image_members(self):
|
||||||
|
self.image_members = [
|
||||||
|
_db_image_member_fixture(UUID4, TENANT2),
|
||||||
|
_db_image_member_fixture(UUID4, TENANT3,
|
||||||
|
status='accepted'),
|
||||||
|
]
|
||||||
|
[self.db.image_member_create(None, image_member)
|
||||||
|
for image_member in self.image_members]
|
||||||
|
|
||||||
def test_index(self):
|
def test_index(self):
|
||||||
self.config(limit_param_default=1, api_limit_max=3)
|
self.config(limit_param_default=1, api_limit_max=3)
|
||||||
request = unit_test_utils.get_fake_request()
|
request = unit_test_utils.get_fake_request()
|
||||||
|
@ -131,6 +150,23 @@ class TestImagesController(test_utils.BaseTestCase):
|
||||||
expected = set([UUID3])
|
expected = set([UUID3])
|
||||||
self.assertEqual(actual, expected)
|
self.assertEqual(actual, expected)
|
||||||
|
|
||||||
|
def test_index_member_status_accepted(self):
|
||||||
|
self.config(limit_param_default=5, api_limit_max=5)
|
||||||
|
request = unit_test_utils.get_fake_request(tenant=TENANT2)
|
||||||
|
output = self.controller.index(request)
|
||||||
|
self.assertEqual(3, len(output['images']))
|
||||||
|
actual = set([image.image_id for image in output['images']])
|
||||||
|
expected = set([UUID1, UUID2, UUID3])
|
||||||
|
# can see only the public image
|
||||||
|
self.assertEqual(actual, expected)
|
||||||
|
|
||||||
|
request = unit_test_utils.get_fake_request(tenant=TENANT3)
|
||||||
|
output = self.controller.index(request)
|
||||||
|
self.assertEqual(4, len(output['images']))
|
||||||
|
actual = set([image.image_id for image in output['images']])
|
||||||
|
expected = set([UUID1, UUID2, UUID3, UUID4])
|
||||||
|
self.assertEqual(actual, expected)
|
||||||
|
|
||||||
def test_index_admin(self):
|
def test_index_admin(self):
|
||||||
request = unit_test_utils.get_fake_request(is_admin=True)
|
request = unit_test_utils.get_fake_request(is_admin=True)
|
||||||
output = self.controller.index(request)
|
output = self.controller.index(request)
|
||||||
|
@ -240,7 +276,8 @@ class TestImagesController(test_utils.BaseTestCase):
|
||||||
self.db.image_create(None, image)
|
self.db.image_create(None, image)
|
||||||
path = '/images?visibility=private'
|
path = '/images?visibility=private'
|
||||||
request = unit_test_utils.get_fake_request(path, is_admin=True)
|
request = unit_test_utils.get_fake_request(path, is_admin=True)
|
||||||
output = self.controller.index(request, filters={'is_public': False})
|
output = self.controller.index(request,
|
||||||
|
filters={'visibility': 'private'})
|
||||||
self.assertEqual(2, len(output['images']))
|
self.assertEqual(2, len(output['images']))
|
||||||
|
|
||||||
def test_index_with_many_filters(self):
|
def test_index_with_many_filters(self):
|
||||||
|
@ -1061,12 +1098,13 @@ class TestImagesDeserializer(test_utils.BaseTestCase):
|
||||||
|
|
||||||
def test_index(self):
|
def test_index(self):
|
||||||
marker = uuidutils.generate_uuid()
|
marker = uuidutils.generate_uuid()
|
||||||
path = '/images?limit=1&marker=%s' % marker
|
path = '/images?limit=1&marker=%s&member_status=pending' % marker
|
||||||
request = unit_test_utils.get_fake_request(path)
|
request = unit_test_utils.get_fake_request(path)
|
||||||
expected = {'limit': 1,
|
expected = {'limit': 1,
|
||||||
'marker': marker,
|
'marker': marker,
|
||||||
'sort_key': 'created_at',
|
'sort_key': 'created_at',
|
||||||
'sort_dir': 'desc',
|
'sort_dir': 'desc',
|
||||||
|
'member_status': 'pending',
|
||||||
'filters': {}}
|
'filters': {}}
|
||||||
output = self.deserializer.index(request)
|
output = self.deserializer.index(request)
|
||||||
self.assertEqual(output, expected)
|
self.assertEqual(output, expected)
|
||||||
|
@ -1112,6 +1150,7 @@ class TestImagesDeserializer(test_utils.BaseTestCase):
|
||||||
request = unit_test_utils.get_fake_request('/images?limit=0')
|
request = unit_test_utils.get_fake_request('/images?limit=0')
|
||||||
expected = {'limit': 0,
|
expected = {'limit': 0,
|
||||||
'sort_key': 'created_at',
|
'sort_key': 'created_at',
|
||||||
|
'member_status': 'accepted',
|
||||||
'sort_dir': 'desc',
|
'sort_dir': 'desc',
|
||||||
'filters': {}}
|
'filters': {}}
|
||||||
output = self.deserializer.index(request)
|
output = self.deserializer.index(request)
|
||||||
|
@ -1127,6 +1166,12 @@ class TestImagesDeserializer(test_utils.BaseTestCase):
|
||||||
self.assertRaises(webob.exc.HTTPBadRequest,
|
self.assertRaises(webob.exc.HTTPBadRequest,
|
||||||
self.deserializer.index, request)
|
self.deserializer.index, request)
|
||||||
|
|
||||||
|
def test_index_invalid_status(self):
|
||||||
|
path = '/images?member_status=blah'
|
||||||
|
request = unit_test_utils.get_fake_request(path)
|
||||||
|
self.assertRaises(webob.exc.HTTPBadRequest,
|
||||||
|
self.deserializer.index, request)
|
||||||
|
|
||||||
def test_index_marker(self):
|
def test_index_marker(self):
|
||||||
marker = uuidutils.generate_uuid()
|
marker = uuidutils.generate_uuid()
|
||||||
path = '/images?marker=%s' % marker
|
path = '/images?marker=%s' % marker
|
||||||
|
@ -1150,6 +1195,7 @@ class TestImagesDeserializer(test_utils.BaseTestCase):
|
||||||
expected = {
|
expected = {
|
||||||
'sort_key': 'id',
|
'sort_key': 'id',
|
||||||
'sort_dir': 'desc',
|
'sort_dir': 'desc',
|
||||||
|
'member_status': 'accepted',
|
||||||
'filters': {}
|
'filters': {}
|
||||||
}
|
}
|
||||||
self.assertEqual(output, expected)
|
self.assertEqual(output, expected)
|
||||||
|
@ -1160,6 +1206,7 @@ class TestImagesDeserializer(test_utils.BaseTestCase):
|
||||||
expected = {
|
expected = {
|
||||||
'sort_key': 'created_at',
|
'sort_key': 'created_at',
|
||||||
'sort_dir': 'asc',
|
'sort_dir': 'asc',
|
||||||
|
'member_status': 'accepted',
|
||||||
'filters': {}}
|
'filters': {}}
|
||||||
self.assertEqual(output, expected)
|
self.assertEqual(output, expected)
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue