Merge "Show snapshots list correctly when launching instance"

This commit is contained in:
Zuul 2018-11-06 23:03:35 +00:00 committed by Gerrit Code Review
commit 799d543206
7 changed files with 189 additions and 17 deletions
openstack_dashboard
dashboards/project
images
instances
static/dashboard/project/workflow/launch-instance
test/test_data

@ -13,6 +13,7 @@
# under the License. # under the License.
from collections import defaultdict from collections import defaultdict
import json
from django.conf import settings from django.conf import settings
from django.template import defaultfilters as filters from django.template import defaultfilters as filters
@ -245,7 +246,14 @@ def get_image_name(image):
def get_image_type(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): def get_format(image):

@ -111,7 +111,7 @@ class ImagesAndSnapshotsTests(BaseImagesTestCase):
self.assertTemplateUsed(res, INDEX_TEMPLATE) self.assertTemplateUsed(res, INDEX_TEMPLATE)
self.assertIn('images_table', res.context) self.assertIn('images_table', res.context)
snaps = res.context['images_table'] 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]) row_actions = snaps.get_row_actions(snaps.data[0])

@ -80,8 +80,10 @@ class InstanceTestBase(helpers.ResetImageAPIVersionMixin,
super(InstanceTestBase, self).setUp() super(InstanceTestBase, self).setUp()
if api.glance.VERSIONS.active < 2: if api.glance.VERSIONS.active < 2:
self.versioned_images = self.images self.versioned_images = self.images
self.versioned_snapshots = self.snapshots
else: else:
self.versioned_images = self.imagesV2 self.versioned_images = self.imagesV2
self.versioned_snapshots = self.snapshotsV2
class InstanceTableTestMixin(object): class InstanceTableTestMixin(object):
@ -2263,6 +2265,111 @@ class InstanceLaunchInstanceTests(InstanceTestBase,
def test_launch_instance_get_with_only_one_network(self): def test_launch_instance_get_with_only_one_network(self):
self.test_launch_instance_get(only_one_network=True) 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', @helpers.create_mocks({api.nova: ('extension_supported',
'is_feature_available', 'is_feature_available',
'flavor_list', 'flavor_list',

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

@ -653,11 +653,21 @@
return bootSourceTypes.NON_BOOTABLE_IMAGE_TYPES.indexOf(image.container_format) < 0; 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) { function onGetImages(data) {
model.images.length = 0; model.images.length = 0;
push.apply(model.images, data.data.items.filter(function (image) { push.apply(model.images, data.data.items.filter(function (image) {
return isBootableImageType(image) && return isBootableImageType(image) && getImageType(image) !== 'snapshot';
(!image.properties || image.properties.image_type !== 'snapshot');
})); }));
addAllowedBootSource(model.images, bootSourceTypes.IMAGE, gettext('Image')); addAllowedBootSource(model.images, bootSourceTypes.IMAGE, gettext('Image'));
} }
@ -665,8 +675,7 @@
function onGetSnapshots(data) { function onGetSnapshots(data) {
model.imageSnapshots.length = 0; model.imageSnapshots.length = 0;
push.apply(model.imageSnapshots, data.data.items.filter(function (image) { push.apply(model.imageSnapshots, data.data.items.filter(function (image) {
return isBootableImageType(image) && return isBootableImageType(image) && getImageType(image) === 'snapshot';
(image.properties && image.properties.image_type === 'snapshot');
})); }));
addAllowedBootSource( addAllowedBootSource(

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

@ -52,6 +52,7 @@ def data(TEST):
TEST.snapshots = utils.TestDataContainer() TEST.snapshots = utils.TestDataContainer()
TEST.metadata_defs = utils.TestDataContainer() TEST.metadata_defs = utils.TestDataContainer()
TEST.imagesV2 = utils.TestDataContainer() TEST.imagesV2 = utils.TestDataContainer()
TEST.snapshotsV2 = utils.TestDataContainer()
# Snapshots # Snapshots
snapshot_dict = {'name': u'snapshot', snapshot_dict = {'name': u'snapshot',
@ -78,12 +79,26 @@ def data(TEST):
'properties': {'image_type': u'snapshot'}, 'properties': {'image_type': u'snapshot'},
'is_public': False, 'is_public': False,
'protected': 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) snapshot = images.Image(images.ImageManager(None), snapshot_dict)
TEST.snapshots.add(api.glance.Image(snapshot)) TEST.snapshots.add(api.glance.Image(snapshot))
snapshot = images.Image(images.ImageManager(None), snapshot_dict_no_owner) snapshot = images.Image(images.ImageManager(None), snapshot_dict_no_owner)
TEST.snapshots.add(api.glance.Image(snapshot)) TEST.snapshots.add(api.glance.Image(snapshot))
snapshot = images.Image(images.ImageManager(None), snapshot_dict_queued) snapshot = images.Image(images.ImageManager(None), snapshot_dict_queued)
TEST.snapshots.add(api.glance.Image(snapshot)) 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 # Images
image_dict = {'id': '007e7d55-fe1e-4c5c-bf08-44b4a4964822', image_dict = {'id': '007e7d55-fe1e-4c5c-bf08-44b4a4964822',
@ -318,6 +333,29 @@ def data(TEST):
apiresource = APIResourceV2(fixture) apiresource = APIResourceV2(fixture)
TEST.imagesV2.add(api.glance.Image(apiresource)) 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 = { metadef_dict = {
'namespace': 'namespace_1', 'namespace': 'namespace_1',
'display_name': 'Namespace 1', 'display_name': 'Namespace 1',