Better support for community images

This commit updates several places where image information is processed
and extends support for the 'community' image visibility value.

Some support did exist already, which was mostly just the main Images
tab of the dashboard, but this commit also includes support for:
  - image name in the instances list/details
  - 'Community' visibility label in the Images tab
  - Listing of community images in launch instance wizard

Closes-Bug: #1779250

Change-Id: Iedea0b7d20313837a72a2759511251a7bb324869
This commit is contained in:
Andy Botting 2018-07-03 16:08:42 +10:00 committed by Akihiro Motoki
parent 2eea0f15b3
commit d641e6d105
13 changed files with 151 additions and 57 deletions

View File

@ -493,7 +493,8 @@ def create_image_metadata(data):
_handle_unknown_properties(data, properties)
if ('visibility' in data and
data['visibility'] not in ['public', 'private', 'shared']):
data['visibility'] not in ['public', 'private', 'community',
'shared']):
raise KeyError('invalid visibility option: %s' % data['visibility'])
_normalize_is_public_filter(data)

View File

@ -67,7 +67,8 @@ class Image(generic.View):
:param min_disk: (optional) the minimum disk size
for the image to boot with
:param min_ram: (optional) the minimum ram for the image to boot with
:param visibility: (required) takes 'public', 'shared', and 'private'
:param visibility: (required) takes 'public', 'shared', 'private' and
'community'
:param protected: (required) true if the image is protected
Any parameters not listed above will be assigned as custom properties
@ -202,7 +203,8 @@ class Images(generic.View):
:param min_disk: (optional) the minimum disk size
for the image to boot with
:param min_ram: (optional) the minimum ram for the image to boot with
:param visibility: (required) takes 'public', 'private', and 'shared'
:param visibility: (required) takes 'public', 'private', 'shared', and
'community'
:param protected: (required) true if the image is protected
:param import_data: (optional) true to copy the image data
to the image service or use it from the current location

View File

@ -399,7 +399,7 @@ class ImageViewTests(test.ResetImageAPIVersionMixin, test.TestCase):
@override_settings(OPENSTACK_API_VERSIONS={'image': 1})
def test_image_detail_custom_props_get_v1(self):
image = self.images.list()[8]
image = self.images.list()[9]
self._test_image_detail_custom_props_get(image)

View File

@ -59,7 +59,7 @@ class ImagesAndSnapshotsTests(BaseImagesTestCase):
images_table = res.context['images_table']
images = images_table.data
self.assertEqual(len(images), 9)
self.assertEqual(len(images), 10)
row_actions = images_table.get_row_actions(images[0])
self.assertEqual(len(row_actions), 5)
row_actions = images_table.get_row_actions(images[1])
@ -155,9 +155,13 @@ class ImagesAndSnapshotsUtilsTests(BaseImagesTestCase):
shared_images = [image for image in self.imagesV2.list()
if (image.status == 'active' and
image.visibility == 'shared')]
community_images = [image for image in self.imagesV2.list()
if (image.status == 'active' and
image.visibility == 'community')]
self.mock_image_list.side_effect = [
[public_images, False, False],
[private_images, False, False],
[community_images, False, False],
[shared_images, False, False]
]
@ -167,6 +171,8 @@ class ImagesAndSnapshotsUtilsTests(BaseImagesTestCase):
mock.call(test.IsHttpRequest(),
filters={'property-owner_id': self.tenant.id,
'status': 'active'}),
mock.call(test.IsHttpRequest(),
filters={'visibility': 'community', 'status': 'active'}),
mock.call(test.IsHttpRequest(),
filters={'visibility': 'shared', 'status': 'active'})
]
@ -186,6 +192,9 @@ class ImagesAndSnapshotsUtilsTests(BaseImagesTestCase):
private_images = [image for image in self.images.list()
if (image.status == 'active' and
not image.is_public)]
community_images = [image for image in self.imagesV2.list()
if (image.status == 'active' and
image.visibility == 'community')]
shared_images = [image for image in self.imagesV2.list()
if (image.status == 'active' and
image.visibility == 'shared')]
@ -193,6 +202,7 @@ class ImagesAndSnapshotsUtilsTests(BaseImagesTestCase):
self.mock_image_list.side_effect = [
[public_images, False, False],
[private_images, False, False],
[community_images, False, False],
[shared_images, False, False],
[private_images, False, False]
]
@ -203,6 +213,8 @@ class ImagesAndSnapshotsUtilsTests(BaseImagesTestCase):
mock.call(test.IsHttpRequest(),
filters={'property-owner_id': self.tenant.id,
'status': 'active'}),
mock.call(test.IsHttpRequest(),
filters={'visibility': 'community', 'status': 'active'}),
mock.call(test.IsHttpRequest(),
filters={'visibility': 'shared', 'status': 'active'}),
mock.call(test.IsHttpRequest(),
@ -225,6 +237,9 @@ class ImagesAndSnapshotsUtilsTests(BaseImagesTestCase):
self.assertEqual(
len(private_images),
len(images_cache['images_by_project'][self.tenant.id]))
self.assertEqual(
len(community_images),
len(images_cache['community_images']))
self.assertEqual(
len(shared_images),
len(images_cache['shared_images']))
@ -252,6 +267,9 @@ class ImagesAndSnapshotsUtilsTests(BaseImagesTestCase):
private_images = [image for image in self.images.list()
if (image.status == 'active' and
not image.is_public)]
community_images = [image for image in self.imagesV2.list()
if (image.status == 'active' and
image.visibility == 'community')]
shared_images = [image for image in self.imagesV2.list()
if (image.status == 'active' and
image.visibility == 'shared')]
@ -259,7 +277,8 @@ class ImagesAndSnapshotsUtilsTests(BaseImagesTestCase):
self.mock_image_list.side_effect = [
self.exceptions.glance,
[private_images, False, False],
[shared_images, False, False],
[community_images, False, False],
[shared_images, False, False]
]
images_cache = {}
@ -270,6 +289,8 @@ class ImagesAndSnapshotsUtilsTests(BaseImagesTestCase):
filters={'is_public': True, 'status': 'active'}),
mock.call(test.IsHttpRequest(),
filters={'status': 'active', 'property-owner_id': '1'}),
mock.call(test.IsHttpRequest(),
filters={'visibility': 'community', 'status': 'active'}),
mock.call(test.IsHttpRequest(),
filters={'visibility': 'shared', 'status': 'active'})
]
@ -286,6 +307,9 @@ class ImagesAndSnapshotsUtilsTests(BaseImagesTestCase):
self.assertEqual(
len(private_images),
len(images_cache['images_by_project'][self.tenant.id]))
self.assertEqual(
len(community_images),
len(images_cache['community_images']))
self.assertEqual(
len(shared_images),
len(images_cache['shared_images']))
@ -297,6 +321,9 @@ class ImagesAndSnapshotsUtilsTests(BaseImagesTestCase):
private_images = [image for image in self.images.list()
if (image.status == 'active' and
not image.is_public)]
community_images = [image for image in self.imagesV2.list()
if (image.status == 'active' and
image.visibility == 'community')]
shared_images = [image for image in self.imagesV2.list()
if (image.status == 'active' and
image.visibility == 'shared')]
@ -304,6 +331,7 @@ class ImagesAndSnapshotsUtilsTests(BaseImagesTestCase):
self.mock_image_list.side_effect = [
[public_images, False, False],
self.exceptions.glance,
[community_images, False, False],
[shared_images, False, False],
[private_images, False, False]
]
@ -317,6 +345,9 @@ class ImagesAndSnapshotsUtilsTests(BaseImagesTestCase):
len(public_images),
len(images_cache['public_images']))
self.assertFalse(len(images_cache['images_by_project']))
self.assertEqual(
len(community_images),
len(images_cache['community_images']))
self.assertEqual(
len(shared_images),
len(images_cache['shared_images']))
@ -334,6 +365,9 @@ class ImagesAndSnapshotsUtilsTests(BaseImagesTestCase):
self.assertEqual(
len(private_images),
len(images_cache['images_by_project'][self.tenant.id]))
self.assertEqual(
len(community_images),
len(images_cache['community_images']))
self.assertEqual(
len(shared_images),
len(images_cache['shared_images']))
@ -343,6 +377,8 @@ class ImagesAndSnapshotsUtilsTests(BaseImagesTestCase):
filters={'status': 'active', 'is_public': True}),
mock.call(test.IsHttpRequest(),
filters={'status': 'active', 'property-owner_id': '1'}),
mock.call(test.IsHttpRequest(),
filters={'status': 'active', 'visibility': 'community'}),
mock.call(test.IsHttpRequest(),
filters={'status': 'active', 'visibility': 'shared'}),
mock.call(test.IsHttpRequest(),

View File

@ -21,9 +21,9 @@ from openstack_dashboard.api import glance
def get_available_images(request, project_id=None, images_cache=None):
"""Returns a list of available images
Returns a list of images that are public, shared or owned by the given
project_id. If project_id is not specified, only public images are
returned.
Returns a list of images that are public, shared, community or owned by
the given project_id. If project_id is not specified, only public and
community images are returned.
:param images_cache: An optional dict-like object in which to
cache public and per-project id image metadata.
@ -32,6 +32,7 @@ def get_available_images(request, project_id=None, images_cache=None):
if images_cache is None:
images_cache = {}
public_images = images_cache.get('public_images', [])
community_images = images_cache.get('community_images', [])
images_by_project = images_cache.get('images_by_project', {})
shared_images = images_cache.get('shared_images', [])
if 'public_images' not in images_cache:
@ -65,6 +66,18 @@ def get_available_images(request, project_id=None, images_cache=None):
else:
owned_images = images_by_project[project_id]
if 'community_images' not in images_cache:
community = {"visibility": "community",
"status": "active"}
try:
images, _more, _prev = glance.image_list_detailed(
request, filters=community)
[community_images.append(image) for image in images]
images_cache['community_images'] = community_images
except Exception:
exceptions.handle(request,
_("Unable to retrieve community images."))
if 'shared_images' not in images_cache:
shared = {"visibility": "shared",
"status": "active"}
@ -79,7 +92,7 @@ def get_available_images(request, project_id=None, images_cache=None):
if 'images_by_project' not in images_cache:
images_cache['images_by_project'] = images_by_project
images = owned_images + public_images + shared_images
images = owned_images + public_images + community_images + shared_images
image_ids = []
final_images = []

View File

@ -2171,7 +2171,7 @@ class InstanceLaunchInstanceTests(InstanceTestBase,
helpers.IsHttpRequest(),
search_opts=SNAPSHOT_SEARCH_OPTS)
self._check_glance_image_list_detailed(count=5)
self._check_glance_image_list_detailed(count=8)
self.mock_network_list.assert_has_calls([
mock.call(helpers.IsHttpRequest(),
@ -2349,7 +2349,7 @@ class InstanceLaunchInstanceTests(InstanceTestBase,
self.mock_volume_snapshot_list.assert_called_once_with(
helpers.IsHttpRequest(),
search_opts=SNAPSHOT_SEARCH_OPTS)
self._check_glance_image_list_detailed(count=5)
self._check_glance_image_list_detailed(count=8)
self.mock_network_list.assert_has_calls([
mock.call(helpers.IsHttpRequest(),
tenant_id=self.tenant.id, shared=False),
@ -2445,7 +2445,7 @@ class InstanceLaunchInstanceTests(InstanceTestBase,
self.mock_volume_snapshot_list.assert_called_once_with(
helpers.IsHttpRequest(),
search_opts=SNAPSHOT_SEARCH_OPTS)
self._check_glance_image_list_detailed(count=5)
self._check_glance_image_list_detailed(count=8)
self.mock_network_list.assert_has_calls([
mock.call(helpers.IsHttpRequest(),
tenant_id=self.tenant.id, shared=False),
@ -2539,7 +2539,7 @@ class InstanceLaunchInstanceTests(InstanceTestBase,
self.assertNoFormErrors(res)
self.assertRedirectsNoFollow(res, INDEX_URL)
self._check_nova_glance_neutron_lists(flavor_count=2, image_count=5)
self._check_nova_glance_neutron_lists(flavor_count=2, image_count=8)
self._check_extension_supported({
'BlockDeviceMappingV2Boot': 1,
'DiskConfig': 1,
@ -2685,7 +2685,7 @@ class InstanceLaunchInstanceTests(InstanceTestBase,
self.assertNoFormErrors(res)
self.assertRedirectsNoFollow(res, INDEX_URL)
self._check_nova_glance_neutron_lists(flavor_count=2, image_count=4)
self._check_nova_glance_neutron_lists(flavor_count=2, image_count=6)
self._check_extension_supported({
'BlockDeviceMappingV2Boot': 2,
'DiskConfig': 1,
@ -2804,7 +2804,7 @@ class InstanceLaunchInstanceTests(InstanceTestBase,
self.assertRedirectsNoFollow(res, INDEX_URL)
self._check_nova_glance_neutron_lists(flavor_count=2,
image_count=4)
image_count=6)
self.assert_mock_multiple_calls_with_same_arguments(
self.mock_flavor_list, 2,
mock.call(helpers.IsHttpRequest()))
@ -2920,7 +2920,7 @@ class InstanceLaunchInstanceTests(InstanceTestBase,
targets=('instances', 'cores', 'ram', 'volumes', 'gigabytes')),
])
self.assertEqual(2, self.mock_tenant_quota_usages.call_count)
self._check_glance_image_list_detailed(count=5)
self._check_glance_image_list_detailed(count=8)
self._check_neutron_network_and_port_list()
self._check_nova_lists(flavor_count=3)
self.mock_volume_list.assert_has_calls([
@ -3137,7 +3137,7 @@ class InstanceLaunchInstanceTests(InstanceTestBase,
self.assertFormErrors(res, 3, "You must select a snapshot.")
self.assertEqual(3, self.mock_image_list_detailed.call_count)
self.assertEqual(4, self.mock_image_list_detailed.call_count)
self.mock_image_list_detailed.assert_has_calls([
mock.call(helpers.IsHttpRequest(),
filters={'is_public': True,
@ -3145,6 +3145,8 @@ class InstanceLaunchInstanceTests(InstanceTestBase,
mock.call(helpers.IsHttpRequest(),
filters={'property-owner_id': self.tenant.id,
'status': 'active'}),
mock.call(helpers.IsHttpRequest(),
filters={'status': 'active', 'visibility': 'community'}),
mock.call(helpers.IsHttpRequest(),
filters={'status': 'active', 'visibility': 'shared'}),
])
@ -3220,7 +3222,7 @@ class InstanceLaunchInstanceTests(InstanceTestBase,
helpers.IsHttpRequest(),
search_opts=SNAPSHOT_SEARCH_OPTS)
self._check_glance_image_list_detailed(count=5)
self._check_glance_image_list_detailed(count=8)
self._check_neutron_network_and_port_list()
self.mock_tenant_quota_usages.assert_called_once_with(
@ -3339,7 +3341,7 @@ class InstanceLaunchInstanceTests(InstanceTestBase,
self.mock_availability_zone_list.assert_called_once_with(
helpers.IsHttpRequest())
self._check_glance_image_list_detailed(count=5)
self._check_glance_image_list_detailed(count=8)
self._check_neutron_network_and_port_list()
self.mock_server_create.assert_called_once_with(
@ -3428,7 +3430,7 @@ class InstanceLaunchInstanceTests(InstanceTestBase,
self.assertContains(res, "greater than or equal to 1")
self._check_nova_glance_neutron_lists(flavor_count=3,
image_count=6)
image_count=10)
self._check_extension_supported({
'BlockDeviceMappingV2Boot': 1,
'DiskConfig': 1,
@ -3539,7 +3541,7 @@ class InstanceLaunchInstanceTests(InstanceTestBase,
self.assertContains(res, msg)
self._check_nova_glance_neutron_lists(flavor_count=3,
image_count=6)
image_count=10)
self._check_extension_supported({
'BlockDeviceMappingV2Boot': 1,
'DiskConfig': 1,
@ -3641,7 +3643,7 @@ class InstanceLaunchInstanceTests(InstanceTestBase,
res = self.client.post(url, form_data)
self._check_nova_glance_neutron_lists(flavor_count=3,
image_count=6)
image_count=10)
self._check_extension_supported({
'BlockDeviceMappingV2Boot': 1,
'DiskConfig': 1,
@ -3802,7 +3804,7 @@ class InstanceLaunchInstanceTests(InstanceTestBase,
helpers.IsHttpRequest())
self.mock_availability_zone_list.assert_called_once_with(
helpers.IsHttpRequest())
self.assertEqual(6, self.mock_image_list_detailed.call_count)
self.assertEqual(10, self.mock_image_list_detailed.call_count)
self.mock_image_list_detailed.assert_has_calls(
[
mock.call(helpers.IsHttpRequest(),
@ -3813,6 +3815,9 @@ class InstanceLaunchInstanceTests(InstanceTestBase,
'status': 'active'})
] +
[
mock.call(helpers.IsHttpRequest(),
filters={'status': 'active',
'visibility': 'community'}),
mock.call(helpers.IsHttpRequest(),
filters={'status': 'active',
'visibility': 'shared'})
@ -3974,9 +3979,9 @@ class InstanceLaunchInstanceTests(InstanceTestBase,
self.mock_availability_zone_list.assert_called_once_with(
helpers.IsHttpRequest())
if avail_volumes is None:
image_list_count = 6
image_list_count = 10
else:
image_list_count = 5
image_list_count = 8
self._check_glance_image_list_detailed(count=image_list_count)
self._check_neutron_network_and_port_list()
self.mock_server_group_list.assert_called_once_with(
@ -4221,7 +4226,7 @@ class InstanceLaunchInstanceTests(InstanceTestBase,
self.assertNoFormErrors(res)
self._check_nova_glance_neutron_lists(flavor_count=2,
image_count=5)
image_count=8)
self._check_extension_supported({
'BlockDeviceMappingV2Boot': 1,
'DiskConfig': 1,
@ -4366,7 +4371,7 @@ class InstanceTests2(InstanceTestBase, InstanceTableTestMixin):
])
self.mock_volume_snapshot_list.assert_called_once_with(
helpers.IsHttpRequest(), search_opts=SNAPSHOT_SEARCH_OPTS)
self._check_glance_image_list_detailed(count=5)
self._check_glance_image_list_detailed(count=8)
self._check_neutron_network_and_port_list()
self.mock_tenant_quota_usages.assert_called_once_with(
helpers.IsHttpRequest(),
@ -4620,7 +4625,7 @@ class InstanceTests2(InstanceTestBase, InstanceTableTestMixin):
self.mock_server_get.assert_called_once_with(
helpers.IsHttpRequest(), server.id)
self._check_glance_image_list_detailed(count=3)
self._check_glance_image_list_detailed(count=4)
self.mock_extension_supported.assert_called_once_with(
'DiskConfig', helpers.IsHttpRequest())
self.mock_is_feature_available.assert_called_once_with(
@ -4674,7 +4679,7 @@ class InstanceTests2(InstanceTestBase, InstanceTableTestMixin):
self.mock_server_get.assert_called_once_with(
helpers.IsHttpRequest(), server.id)
self._check_glance_image_list_detailed(count=3)
self._check_glance_image_list_detailed(count=4)
self.mock_extension_supported.assert_called_once_with(
'DiskConfig', helpers.IsHttpRequest())
self.mock_server_rebuild.assert_called_once_with(
@ -4703,7 +4708,7 @@ class InstanceTests2(InstanceTestBase, InstanceTableTestMixin):
self.mock_server_get.assert_called_once_with(
helpers.IsHttpRequest(), server.id)
self._check_glance_image_list_detailed(count=3)
self._check_glance_image_list_detailed(count=4)
self.mock_extension_supported.assert_called_once_with(
'DiskConfig', helpers.IsHttpRequest())
self.mock_server_rebuild.assert_called_once_with(
@ -4734,10 +4739,10 @@ class InstanceTests2(InstanceTestBase, InstanceTableTestMixin):
["Passwords do not match."])
if django.VERSION >= (1, 9):
image_list_count = 6
image_list_count = 8
ext_count = 2
else:
image_list_count = 3
image_list_count = 5
ext_count = 1
self.mock_server_get.assert_called_once_with(
helpers.IsHttpRequest(), server.id)
@ -4769,7 +4774,7 @@ class InstanceTests2(InstanceTestBase, InstanceTableTestMixin):
self.mock_server_get.assert_called_once_with(
helpers.IsHttpRequest(), server.id)
self._check_glance_image_list_detailed(count=3)
self._check_glance_image_list_detailed(count=4)
self.mock_extension_supported.assert_called_once_with(
'DiskConfig', helpers.IsHttpRequest())
self.mock_server_rebuild.assert_called_once_with(
@ -4802,7 +4807,7 @@ class InstanceTests2(InstanceTestBase, InstanceTableTestMixin):
self.mock_server_get.assert_called_once_with(
helpers.IsHttpRequest(), server.id)
self._check_glance_image_list_detailed(count=3)
self._check_glance_image_list_detailed(count=4)
self.mock_extension_supported.assert_called_once_with(
'DiskConfig', helpers.IsHttpRequest())
self.mock_server_rebuild.assert_called_once_with(
@ -4832,7 +4837,7 @@ class InstanceTests2(InstanceTestBase, InstanceTableTestMixin):
self.mock_server_get.assert_called_once_with(
helpers.IsHttpRequest(), server.id)
self._check_glance_image_list_detailed(count=3)
self._check_glance_image_list_detailed(count=4)
self.mock_extension_supported.assert_called_once_with(
'DiskConfig', helpers.IsHttpRequest())
self.mock_server_rebuild.assert_called_once_with(
@ -5045,7 +5050,7 @@ class InstanceTests2(InstanceTestBase, InstanceTableTestMixin):
password=password)
self.assertNoFormErrors(res)
self.assertRedirectsNoFollow(res, INDEX_URL)
self._check_glance_image_list_detailed(count=3)
self._check_glance_image_list_detailed(count=4)
self.mock_server_rescue.assert_called_once_with(
helpers.IsHttpRequest(), server.id, image=image.id,
password=password)

View File

@ -563,7 +563,14 @@
if (enabledImage || enabledSnapshot) {
var filter = {status: 'active', sort_key: 'name', sort_dir: 'asc'};
return glanceAPI.getImages(filter).then(function getEnabledImages(data) {
var filterCommunity = angular.merge({}, filter, {visibility: 'community'});
var imagePromises = [
glanceAPI.getImages(filter),
glanceAPI.getImages(filterCommunity)
];
$q.all(imagePromises).then(function getEnabledImages(data) {
if (enabledImage) {
onGetImages(data);
}
@ -666,18 +673,22 @@
function onGetImages(data) {
model.images.length = 0;
push.apply(model.images, data.data.items.filter(function (image) {
return isBootableImageType(image) && getImageType(image) !== 'snapshot';
}));
angular.forEach(data, function addData(data) {
push.apply(model.images, data.data.items.filter(function (image) {
return isBootableImageType(image) &&
(!image.properties || image.properties.image_type !== 'snapshot');
}));
});
addAllowedBootSource(model.images, bootSourceTypes.IMAGE, gettext('Image'));
}
function onGetSnapshots(data) {
model.imageSnapshots.length = 0;
push.apply(model.imageSnapshots, data.data.items.filter(function (image) {
return isBootableImageType(image) && getImageType(image) === 'snapshot';
}));
angular.forEach(data, function addData(data) {
push.apply(model.imageSnapshots, data.data.items.filter(function (image) {
return isBootableImageType(image) && getImageType(image) === 'snapshot';
}));
});
addAllowedBootSource(
model.imageSnapshots,
bootSourceTypes.INSTANCE_SNAPSHOT,

View File

@ -430,8 +430,8 @@
expect(model.initialized).toBe(true);
expect(model.newInstanceSpec).toBeDefined();
expect(model.images.length).toBe(4);
expect(model.imageSnapshots.length).toBe(2);
expect(model.images.length).toBe(12);
expect(model.imageSnapshots.length).toBe(4);
expect(model.availabilityZones.length).toBe(3); // 2 + 1 for 'nova pick'
expect(model.flavors.length).toBe(2);
expect(model.keypairs.length).toBe(2);

View File

@ -162,7 +162,11 @@
};
// Map Visibility data so we can decode true/false to Public/Private
var _visibilitymap = { true: gettext('Public'), false: gettext('Private') };
var _visibilitymap = { 'public': gettext('Public'),
'private': gettext('Private'),
'shared': gettext('Shared'),
'community': gettext('Community')
};
// Mapping for dynamic table data
var tableBodyCellsMap = {
@ -171,14 +175,14 @@
{ key: 'updated_at', filter: dateFilter, filterArg: 'short' },
{ key: 'size', filter: bytesFilter, classList: ['number'] },
{ key: 'disk_format', filter: diskFormatFilter, filterRawData: true },
{ key: 'is_public', filter: decodeFilter, filterArg: _visibilitymap }
{ key: 'visibility', filter: decodeFilter, filterArg: _visibilitymap }
],
snapshot: [
{ key: 'name', classList: ['hi-light', 'word-break'] },
{ key: 'updated_at', filter: dateFilter, filterArg: 'short' },
{ key: 'size', filter: bytesFilter, classList: ['number'] },
{ key: 'disk_format', filter: diskFormatFilter, filterRawData: true },
{ key: 'is_public', filter: decodeFilter, filterArg: _visibilitymap }
{ key: 'visibility', filter: decodeFilter, filterArg: _visibilitymap }
],
volume: [
{ key: 'name', classList: ['hi-light', 'word-break'] },
@ -273,11 +277,13 @@
},
visibility: {
label: gettext('Visibility'),
name: 'is_public',
name: 'visibility',
singleton: true,
options: [
{ label: gettext('Public'), key: 'true' },
{ label: gettext('Private'), key: 'false' }
{ label: gettext('Public'), key: 'public' },
{ label: gettext('Private'), key: 'private' },
{ label: gettext('Shared With Project'), key: 'shared' },
{ label: gettext('Community'), key: 'community' }
]
},
volumeType: {

View File

@ -154,7 +154,7 @@
expect(ctrl.sourceFacets[1].name).toEqual('updated_at');
expect(ctrl.sourceFacets[2].name).toEqual('size');
expect(ctrl.sourceFacets[3].name).toEqual('disk_format');
expect(ctrl.sourceFacets[4].name).toEqual('is_public');
expect(ctrl.sourceFacets[4].name).toEqual('visibility');
});
it('should broadcast event when source type is changed', function() {
@ -174,7 +174,7 @@
expect(ctrl.sourceFacets[1].name).toEqual('updated_at');
expect(ctrl.sourceFacets[2].name).toEqual('size');
expect(ctrl.sourceFacets[3].name).toEqual('disk_format');
expect(ctrl.sourceFacets[4].name).toEqual('is_public');
expect(ctrl.sourceFacets[4].name).toEqual('visibility');
});
it('should change facets for volume source type', function() {

View File

@ -119,6 +119,8 @@
function deriveSharingStatus(image, currentProjectId, translatedVisibility) {
if (angular.equals(translatedVisibility, imageVisibility.public)) {
return translatedVisibility;
} else if (angular.equals(translatedVisibility, imageVisibility.community)) {
return translatedVisibility;
} else if (angular.isDefined(currentProjectId) &&
!angular.equals(image.owner, currentProjectId)) {
return imageVisibility.other;

View File

@ -186,6 +186,22 @@ def data(TEST):
'min_ram': 0}
private_image3 = images.Image(images.ImageManager(None), image_dict)
# A community image. Not public and not local tenant, but visibility
# is set as 'community'
image_dict = {'id': '13682b34-fb85-4fe5-bd65-1e16305b45c9',
'name': 'community_image 1',
'status': "active",
'size': 10 * 1024 ** 3,
'virtual_size': None,
'min_disk': 0,
'owner': 'someothertenant',
'container_format': 'aki',
'is_public': False,
'protected': False,
'min_ram': 0,
'visibility': 'community'}
community_image = images.Image(images.ImageManager(None), image_dict)
# A shared image. Not public and not local tenant.
image_dict = {'id': 'c8756975-7a3b-4e43-b7f7-433576112849',
'name': 'shared_image 1',
@ -244,7 +260,8 @@ def data(TEST):
TEST.images_api.add(public_image, private_image, protected_image,
public_image2, private_image2, private_image3,
shared_image1, official_image1, multi_prop_image)
community_image, shared_image1, official_image1,
multi_prop_image)
TEST.images.add(api.glance.Image(public_image),
api.glance.Image(private_image),
@ -252,6 +269,7 @@ def data(TEST):
api.glance.Image(public_image2),
api.glance.Image(private_image2),
api.glance.Image(private_image3),
api.glance.Image(community_image),
api.glance.Image(shared_image1),
api.glance.Image(official_image1),
api.glance.Image(multi_prop_image))

View File

@ -191,7 +191,7 @@ class GlanceApiTests(test.APIMockTestCase):
self.assertFalse(has_more)
self.assertFalse(has_prev)
@override_settings(API_RESULT_PAGE_SIZE=9)
@override_settings(API_RESULT_PAGE_SIZE=10)
@mock.patch.object(api.glance, 'glanceclient')
def test_image_list_detailed_pagination_equal_page_size(self,
mock_glanceclient):