Show snapshots list correctly when launching instance

In launch instance modal, when a user selects 'Instance Snapshots',
not all snapshots are listed. The snapshots which are created from
an instance with new volume or an instance created from volume
or volume snapshot don't have 'image_type' but 'block_device_mapping'.
So, judging only by image_type is not enough.

Change-Id: I7e175b6a7260ca3d82560427a8f742f8cfa35565
Closes-Bug: #1627619
This commit is contained in:
wangliangyu 2018-02-26 18:05:25 +08:00 committed by Akihiro Motoki
parent fa3f856f02
commit 5f4057f8b5
7 changed files with 189 additions and 17 deletions

View File

@ -13,6 +13,7 @@
# under the License.
from collections import defaultdict
import json
from django.conf import settings
from django.template import defaultfilters as filters
@ -245,7 +246,14 @@ def get_image_name(image):
def get_image_type(image):
return getattr(image, "properties", {}).get("image_type", "image")
if not hasattr(image, 'properties'):
return 'image'
if image.properties.get('image_type'):
return image.properties.get('image_type')
if image.properties.get('block_device_mapping'):
block_device_mapping = image.properties.get('block_device_mapping')
return json.loads(block_device_mapping)[0].get('source_type')
return 'image'
def get_format(image):

View File

@ -111,7 +111,7 @@ class ImagesAndSnapshotsTests(BaseImagesTestCase):
self.assertTemplateUsed(res, INDEX_TEMPLATE)
self.assertIn('images_table', res.context)
snaps = res.context['images_table']
self.assertEqual(len(snaps.get_rows()), 3)
self.assertEqual(len(snaps.get_rows()), 4)
row_actions = snaps.get_row_actions(snaps.data[0])

View File

@ -80,8 +80,10 @@ class InstanceTestBase(helpers.ResetImageAPIVersionMixin,
super(InstanceTestBase, self).setUp()
if api.glance.VERSIONS.active < 2:
self.versioned_images = self.images
self.versioned_snapshots = self.snapshots
else:
self.versioned_images = self.imagesV2
self.versioned_snapshots = self.snapshotsV2
class InstanceTableTestMixin(object):
@ -2263,6 +2265,111 @@ class InstanceLaunchInstanceTests(InstanceTestBase,
def test_launch_instance_get_with_only_one_network(self):
self.test_launch_instance_get(only_one_network=True)
@helpers.create_mocks({api.nova: ('extension_supported',
'is_feature_available',
'flavor_list',
'keypair_list',
'server_group_list',
'availability_zone_list',),
cinder: ('volume_snapshot_list',
'volume_list',),
api.neutron: ('network_list',
'port_list_with_trunk_types',
'security_group_list',),
api.glance: ('image_list_detailed',),
quotas: ('tenant_quota_usages',)})
def test_launch_instance_get_images_snapshots(self,
block_device_mapping_v2=True,
only_one_network=False,
disk_config=True,
config_drive=True):
self._mock_extension_supported({
'BlockDeviceMappingV2Boot': block_device_mapping_v2,
'DiskConfig': disk_config,
'ConfigDrive': config_drive,
'ServerGroups': True,
})
self.mock_volume_list.return_value = []
self.mock_volume_snapshot_list.return_value = []
self._mock_glance_image_list_detailed(self.versioned_images.list() +
self.versioned_snapshots.list())
self.mock_network_list.side_effect = [
self.networks.list()[:1],
[] if only_one_network else self.networks.list()[1:],
self.networks.list()[:1],
self.networks.list()[1:],
]
self.mock_port_list_with_trunk_types.return_value = self.ports.list()
self.mock_server_group_list.return_value = self.server_groups.list()
self.mock_tenant_quota_usages.return_value = self.limits['absolute']
self._mock_nova_lists()
url = reverse('horizon:project:instances:launch')
res = self.client.get(url)
image_sources = (res.context_data['workflow'].steps[0].
action.fields['image_id'].choices)
snapshot_sources = (res.context_data['workflow'].steps[0].
action.fields['instance_snapshot_id'].choices)
images = [image.id for image in self.versioned_images.list()]
snapshots = [s.id for s in self.versioned_snapshots.list()]
image_sources_ids = []
snapshot_sources_ids = []
for image in image_sources:
self.assertTrue(image[0] in images or image[0] == '')
if image[0] != '':
image_sources_ids.append(image[0])
for image in images:
self.assertIn(image, image_sources_ids)
for snapshot in snapshot_sources:
self.assertTrue(snapshot[0] in snapshots or snapshot[0] == '')
if snapshot[0] != '':
snapshot_sources_ids.append(snapshot[0])
for snapshot in snapshots:
self.assertIn(snapshot, snapshot_sources_ids)
self._check_extension_supported({
'BlockDeviceMappingV2Boot': 1,
'DiskConfig': 1,
'ConfigDrive': 1,
'ServerGroups': 1,
})
self.mock_volume_list.assert_has_calls([
mock.call(helpers.IsHttpRequest(),
search_opts=VOLUME_SEARCH_OPTS),
mock.call(helpers.IsHttpRequest(),
search_opts=VOLUME_BOOTABLE_SEARCH_OPTS),
])
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.mock_network_list.assert_has_calls([
mock.call(helpers.IsHttpRequest(),
tenant_id=self.tenant.id, shared=False),
mock.call(helpers.IsHttpRequest(), shared=True),
mock.call(helpers.IsHttpRequest(),
tenant_id=self.tenant.id, shared=False),
mock.call(helpers.IsHttpRequest(), shared=True),
])
self.assertEqual(4, self.mock_network_list.call_count)
self.mock_port_list_with_trunk_types.assert_has_calls(
[mock.call(helpers.IsHttpRequest(),
network_id=net.id, tenant_id=self.tenant.id)
for net in self.networks.list()])
self.mock_server_group_list.assert_called_once_with(
helpers.IsHttpRequest())
self.mock_tenant_quota_usages.assert_called_once_with(
helpers.IsHttpRequest(),
targets=('instances', 'cores', 'ram', 'volumes', 'gigabytes'))
self._check_nova_lists(flavor_count=2)
@helpers.create_mocks({api.nova: ('extension_supported',
'is_feature_available',
'flavor_list',

View File

@ -41,6 +41,8 @@ from openstack_dashboard.api import cinder
from openstack_dashboard.api import nova
from openstack_dashboard.usage import quotas
from openstack_dashboard.dashboards.project.images.images \
import tables as image_tables
from openstack_dashboard.dashboards.project.images \
import utils as image_utils
from openstack_dashboard.dashboards.project.instances \
@ -439,7 +441,7 @@ class SetInstanceDetailsAction(workflows.Action):
context.get('project_id'),
self._images_cache)
for image in images:
if image.properties.get("image_type", '') != "snapshot":
if image_tables.get_image_type(image) != "snapshot":
image.bytes = getattr(
image, 'virtual_size', None) or image.size
image.volume_size = max(
@ -461,7 +463,7 @@ class SetInstanceDetailsAction(workflows.Action):
self._images_cache)
choices = [(image.id, image.name)
for image in images
if image.properties.get("image_type", '') == "snapshot"]
if image_tables.get_image_type(image) == "snapshot"]
if choices:
choices.sort(key=operator.itemgetter(1))
choices.insert(0, ("", _("Select Instance Snapshot")))

View File

@ -653,11 +653,21 @@
return bootSourceTypes.NON_BOOTABLE_IMAGE_TYPES.indexOf(image.container_format) < 0;
}
function getImageType(image) {
if (image === null || !angular.isDefined(image.properties) ||
!(angular.isDefined(image.properties.image_type) ||
angular.isDefined(image.properties.block_device_mapping))) {
return 'image';
}
return image.properties.image_type ||
angular.fromJson(image.properties.block_device_mapping)[0].source_type ||
'image';
}
function onGetImages(data) {
model.images.length = 0;
push.apply(model.images, data.data.items.filter(function (image) {
return isBootableImageType(image) &&
(!image.properties || image.properties.image_type !== 'snapshot');
return isBootableImageType(image) && getImageType(image) !== 'snapshot';
}));
addAllowedBootSource(model.images, bootSourceTypes.IMAGE, gettext('Image'));
}
@ -665,8 +675,7 @@
function onGetSnapshots(data) {
model.imageSnapshots.length = 0;
push.apply(model.imageSnapshots, data.data.items.filter(function (image) {
return isBootableImageType(image) &&
(image.properties && image.properties.image_type === 'snapshot');
return isBootableImageType(image) && getImageType(image) === 'snapshot';
}));
addAllowedBootSource(

View File

@ -137,8 +137,12 @@
{container_format: 'ari', properties: {}},
{container_format: 'ami', properties: {}},
{container_format: 'raw', properties: {}},
{container_format: 'ami', properties: {image_type: 'snapshot'}},
{container_format: 'raw', properties: {image_type: 'snapshot'}}
{container_format: 'ami', properties: {image_type: 'image'}},
{container_format: 'raw', properties: {image_type: 'image'}},
{container_format: 'ami', properties: {
block_device_mapping: '[{"source_type": "snapshot"}]'}},
{container_format: 'raw', properties: {
block_device_mapping: '[{"source_type": "snapshot"}]'}}
];
var deferred = $q.defer();
@ -183,12 +187,16 @@
$provide.value('horizon.app.core.openstack-service-api.glance', {
getImages: function() {
var images = [
{ container_format: 'aki', properties: {} },
{ container_format: 'ari', properties: {} },
{ container_format: 'ami', properties: {} },
{ container_format: 'raw', properties: {} },
{ container_format: 'ami', properties: { image_type: 'snapshot' } },
{ container_format: 'raw', properties: { image_type: 'snapshot' } }
{container_format: 'aki', properties: {} },
{container_format: 'ari', properties: {} },
{container_format: 'ami', properties: {} },
{container_format: 'raw', properties: {} },
{container_format: 'ami', properties: {image_type: 'image'}},
{container_format: 'raw', properties: {image_type: 'image'}},
{container_format: 'ami', properties: {
block_device_mapping: '[{"source_type": "snapshot"}]'}},
{container_format: 'raw', properties: {
block_device_mapping: '[{"source_type": "snapshot"}]'}}
];
var deferred = $q.defer();
@ -422,7 +430,7 @@
expect(model.initialized).toBe(true);
expect(model.newInstanceSpec).toBeDefined();
expect(model.images.length).toBe(2);
expect(model.images.length).toBe(4);
expect(model.imageSnapshots.length).toBe(2);
expect(model.availabilityZones.length).toBe(3); // 2 + 1 for 'nova pick'
expect(model.flavors.length).toBe(2);

View File

@ -52,6 +52,7 @@ def data(TEST):
TEST.snapshots = utils.TestDataContainer()
TEST.metadata_defs = utils.TestDataContainer()
TEST.imagesV2 = utils.TestDataContainer()
TEST.snapshotsV2 = utils.TestDataContainer()
# Snapshots
snapshot_dict = {'name': u'snapshot',
@ -78,12 +79,26 @@ def data(TEST):
'properties': {'image_type': u'snapshot'},
'is_public': False,
'protected': False}
snapshot_dict_with_volume = {'name': u'snapshot 2',
'container_format': u'ami',
'id': 6,
'status': "queued",
'owner': TEST.tenant.id,
'properties': {
'block_device_mapping':
'[{"source_type": "snapshot"}]'},
'is_public': False,
'protected': False}
snapshot = images.Image(images.ImageManager(None), snapshot_dict)
TEST.snapshots.add(api.glance.Image(snapshot))
snapshot = images.Image(images.ImageManager(None), snapshot_dict_no_owner)
TEST.snapshots.add(api.glance.Image(snapshot))
snapshot = images.Image(images.ImageManager(None), snapshot_dict_queued)
TEST.snapshots.add(api.glance.Image(snapshot))
snapshot = images.Image(images.ImageManager(None),
snapshot_dict_with_volume)
TEST.snapshots.add(api.glance.Image(snapshot))
# Images
image_dict = {'id': '007e7d55-fe1e-4c5c-bf08-44b4a4964822',
@ -318,6 +333,29 @@ def data(TEST):
apiresource = APIResourceV2(fixture)
TEST.imagesV2.add(api.glance.Image(apiresource))
snapshot_v2_dict = {
'checksum': None,
'container_format': 'novaImage',
'created_at': '2018-02-26T22:50:56Z',
'disk_format': None,
'block_device_mapping': '[{"source_type": "snapshot"}]',
'file': '/v2/images/c701226a-aa32-4064-bd36-e85a3dcc61aa/file',
'id': 'c701226a-aa32-4064-bd36-e85a3dcc61aa',
'locations': [],
'min_disk': 30,
'min_ram': 0,
'name': 'snpashot_with_volume',
'owner': TEST.tenant.id,
'protected': True,
'size': 2 * 1024 ** 3,
'status': "active",
'tags': ['empty_image'],
'updated_at': '2018-02-26T22:50:56Z',
'virtual_size': None,
'visibility': 'public'
}
TEST.snapshotsV2.add(api.glance.Image(APIResourceV2(snapshot_v2_dict)))
metadef_dict = {
'namespace': 'namespace_1',
'display_name': 'Namespace 1',