Merge "Add support to get disk_formats from glance"

This commit is contained in:
Zuul 2019-12-19 15:19:56 +00:00 committed by Gerrit Code Review
commit 3a829be260
12 changed files with 232 additions and 20 deletions

View File

@ -29,11 +29,13 @@ from django.conf import settings
from django.core.files.uploadedfile import InMemoryUploadedFile from django.core.files.uploadedfile import InMemoryUploadedFile
from django.core.files.uploadedfile import SimpleUploadedFile from django.core.files.uploadedfile import SimpleUploadedFile
from django.core.files.uploadedfile import TemporaryUploadedFile from django.core.files.uploadedfile import TemporaryUploadedFile
from django.utils.translation import ugettext_lazy as _
from glanceclient.v2 import client from glanceclient.v2 import client
import six import six
from six.moves import _thread as thread from six.moves import _thread as thread
from horizon import messages
from horizon.utils.memoized import memoized from horizon.utils.memoized import memoized
from openstack_dashboard.api import base from openstack_dashboard.api import base
from openstack_dashboard.contrib.developer.profiler import api as profiler from openstack_dashboard.contrib.developer.profiler import api as profiler
@ -345,6 +347,32 @@ def image_update(request, image_id, **kwargs):
{'file': filename, 'e': e}) {'file': filename, 'e': e})
def get_image_formats(request):
image_format_choices = settings.OPENSTACK_IMAGE_BACKEND['image_formats']
try:
glance_schemas = get_image_schemas(request)
glance_formats = \
glance_schemas['properties']['disk_format']['enum']
supported_formats = []
for value, name in image_format_choices:
if value in glance_formats:
supported_formats.append((value, name))
else:
LOG.warning('OPENSTACK_IMAGE_BACKEND has a format "%s" '
'unsupported by glance', value)
except Exception:
supported_formats = image_format_choices
msg = _('Unable to retrieve image format list.')
messages.error(request, msg)
return supported_formats
@profiler.trace
def get_image_schemas(request):
return glanceclient(request).schemas.get('image').raw()
def get_image_upload_mode(): def get_image_upload_mode():
mode = settings.HORIZON_IMAGES_UPLOAD_MODE mode = settings.HORIZON_IMAGES_UPLOAD_MODE
if mode not in ('off', 'legacy', 'direct'): if mode not in ('off', 'legacy', 'direct'):

View File

@ -58,8 +58,18 @@ class Settings(generic.View):
plain_settings = {k: getattr(settings, k, None) for k plain_settings = {k: getattr(settings, k, None) for k
in settings_allowed if k not in self.SPECIALS} in settings_allowed if k not in self.SPECIALS}
plain_settings.update(self.SPECIALS) plain_settings.update(self.SPECIALS)
plain_settings.update(self.disk_formats(request))
return plain_settings return plain_settings
def disk_formats(self, request):
# The purpose of OPENSTACK_IMAGE_FORMATS is to provide a simple object
# that does not contain the lazy-loaded translations, so the list can
# be sent as JSON to the client-side (Angular).
return {'OPENSTACK_IMAGE_FORMATS': [
value
for (value, name) in api.glance.get_image_formats(request)
]}
@urls.register @urls.register
class Timezones(generic.View): class Timezones(generic.View):

View File

@ -27,12 +27,15 @@ INDEX_TEMPLATE = 'horizon/common/_data_table_view.html'
class ImageCreateViewTest(test.BaseAdminViewTests): class ImageCreateViewTest(test.BaseAdminViewTests):
@mock.patch.object(api.glance, 'get_image_schemas')
@mock.patch.object(api.glance, 'image_list_detailed') @mock.patch.object(api.glance, 'image_list_detailed')
def test_admin_image_create_view_uses_admin_template(self, def test_admin_image_create_view_uses_admin_template(self,
mock_image_list): mock_image_list,
mock_schemas_list):
filters1 = {'disk_format': 'aki'} filters1 = {'disk_format': 'aki'}
filters2 = {'disk_format': 'ari'} filters2 = {'disk_format': 'ari'}
mock_schemas_list.return_value = self.image_schemas.first()
mock_image_list.return_value = [self.images.list(), False, False] mock_image_list.return_value = [self.images.list(), False, False]
res = self.client.get( res = self.client.get(

View File

@ -174,7 +174,8 @@ class CreateImageForm(CreateParent):
if not policy.check((("image", "publicize_image"),), request): if not policy.check((("image", "publicize_image"),), request):
self._hide_is_public() self._hide_is_public()
self.fields['disk_format'].choices = IMAGE_FORMAT_CHOICES self.fields['disk_format'].choices = \
api.glance.get_image_formats(request)
try: try:
kernel_images = api.glance.image_list_detailed( kernel_images = api.glance.image_list_detailed(

View File

@ -38,8 +38,9 @@ IMAGES_INDEX_URL = reverse('horizon:project:images:index')
class CreateImageFormTests(test.ResetImageAPIVersionMixin, test.TestCase): class CreateImageFormTests(test.ResetImageAPIVersionMixin, test.TestCase):
@mock.patch.object(api.glance, 'get_image_formats')
@mock.patch.object(api.glance, 'image_list_detailed') @mock.patch.object(api.glance, 'image_list_detailed')
def test_no_location_or_file(self, mock_image_list): def test_no_location_or_file(self, mock_image_list, mock_schemas_list):
mock_image_list.side_effect = [ mock_image_list.side_effect = [
[self.images.list(), False, False], [self.images.list(), False, False],
[self.images.list(), False, False] [self.images.list(), False, False]
@ -133,8 +134,9 @@ class UpdateImageFormTests(test.ResetImageAPIVersionMixin, test.TestCase):
class ImageViewTests(test.ResetImageAPIVersionMixin, test.TestCase): class ImageViewTests(test.ResetImageAPIVersionMixin, test.TestCase):
@mock.patch.object(api.glance, 'get_image_schemas')
@mock.patch.object(api.glance, 'image_list_detailed') @mock.patch.object(api.glance, 'image_list_detailed')
def test_image_create_get(self, mock_image_list): def test_image_create_get(self, mock_image_list, mock_schemas_list):
mock_image_list.side_effect = [ mock_image_list.side_effect = [
[self.images.list(), False, False], [self.images.list(), False, False],
[self.images.list(), False, False] [self.images.list(), False, False]
@ -151,7 +153,9 @@ class ImageViewTests(test.ResetImageAPIVersionMixin, test.TestCase):
mock_image_list.assert_has_calls(image_calls) mock_image_list.assert_has_calls(image_calls)
@override_settings(IMAGES_ALLOW_LOCATION=True) @override_settings(IMAGES_ALLOW_LOCATION=True)
def test_image_create_post_location_v2(self): @mock.patch.object(api.glance, 'get_image_schemas')
def test_image_create_post_location_v2(self, mock_schemas_list):
mock_schemas_list.return_value = self.image_schemas.first()
data = { data = {
'source_type': u'url', 'source_type': u'url',
'image_url': u'http://cloud-images.ubuntu.com/releases/' 'image_url': u'http://cloud-images.ubuntu.com/releases/'
@ -161,7 +165,9 @@ class ImageViewTests(test.ResetImageAPIVersionMixin, test.TestCase):
api_data = {'location': data['image_url']} api_data = {'location': data['image_url']}
self._test_image_create(data, api_data) self._test_image_create(data, api_data)
def test_image_create_post_upload_v2(self): @mock.patch.object(api.glance, 'get_image_schemas')
def test_image_create_post_upload_v2(self, mock_schemas_list):
mock_schemas_list.return_value = self.image_schemas.first()
temp_file = tempfile.NamedTemporaryFile() temp_file = tempfile.NamedTemporaryFile()
temp_file.write(b'123') temp_file.write(b'123')
temp_file.flush() temp_file.flush()
@ -173,7 +179,9 @@ class ImageViewTests(test.ResetImageAPIVersionMixin, test.TestCase):
api_data = {'data': test.IsA(InMemoryUploadedFile)} api_data = {'data': test.IsA(InMemoryUploadedFile)}
self._test_image_create(data, api_data) self._test_image_create(data, api_data)
def test_image_create_post_with_kernel_ramdisk_v2(self): @mock.patch.object(api.glance, 'get_image_schemas')
def test_image_create_post_with_kernel_ramdisk_v2(self, mock_schemas_list):
mock_schemas_list.return_value = self.image_schemas.first()
temp_file = tempfile.NamedTemporaryFile() temp_file = tempfile.NamedTemporaryFile()
temp_file.write(b'123') temp_file.write(b'123')
temp_file.flush() temp_file.flush()

View File

@ -719,7 +719,7 @@ class UploadToImageForm(forms.SelfHandlingForm):
# I can only use 'raw', 'vmdk', 'vdi' or 'qcow2' so qemu-img will not # I can only use 'raw', 'vmdk', 'vdi' or 'qcow2' so qemu-img will not
# have issues when processes image request from cinder. # have issues when processes image request from cinder.
disk_format_choices = [(value, name) for value, name disk_format_choices = [(value, name) for value, name
in IMAGE_FORMAT_CHOICES in glance.get_image_formats(request)
if value in VALID_DISK_FORMATS] if value in VALID_DISK_FORMATS]
self.fields['disk_format'].choices = disk_format_choices self.fields['disk_format'].choices = disk_format_choices
self.fields['disk_format'].initial = 'raw' self.fields['disk_format'].initial = 'raw'

View File

@ -1668,9 +1668,10 @@ class VolumeViewTests(test.ResetImageAPIVersionMixin, test.TestCase):
self.mock_volume_set_bootable.assert_called_once_with( self.mock_volume_set_bootable.assert_called_once_with(
test.IsHttpRequest(), volume.id, True) test.IsHttpRequest(), volume.id, True)
@mock.patch.object(api.glance, 'get_image_schemas')
@mock.patch.object(cinder, 'volume_upload_to_image') @mock.patch.object(cinder, 'volume_upload_to_image')
@mock.patch.object(cinder, 'volume_get') @mock.patch.object(cinder, 'volume_get')
def test_upload_to_image(self, mock_get, mock_upload): def test_upload_to_image(self, mock_get, mock_upload, mock_schemas_list):
volume = self.cinder_volumes.get(name='v2_volume') volume = self.cinder_volumes.get(name='v2_volume')
loaded_resp = {'container_format': 'bare', loaded_resp = {'container_format': 'bare',
'disk_format': 'raw', 'disk_format': 'raw',
@ -1687,6 +1688,7 @@ class VolumeViewTests(test.ResetImageAPIVersionMixin, test.TestCase):
'container_format': 'bare', 'container_format': 'bare',
'disk_format': 'raw'} 'disk_format': 'raw'}
mock_schemas_list.return_value = self.image_schemas.first()
mock_get.return_value = volume mock_get.return_value = volume
mock_upload.return_value = loaded_resp mock_upload.return_value = loaded_resp

View File

@ -278,14 +278,6 @@ if os.path.exists(LOCAL_SETTINGS_DIR_PATH):
_LOG.exception( _LOG.exception(
"Can not exec settings snippet %s", filename) "Can not exec settings snippet %s", filename)
# The purpose of OPENSTACK_IMAGE_FORMATS is to provide a simple object
# that does not contain the lazy-loaded translations, so the list can
# be sent as JSON to the client-side (Angular).
# TODO(amotoki): Do we really need this here? Can't we calculate this
# in openstack_dashboard.api.rest.config?
OPENSTACK_IMAGE_FORMATS = [fmt for (fmt, name)
in OPENSTACK_IMAGE_BACKEND['image_formats']]
if USER_MENU_LINKS is None: if USER_MENU_LINKS is None:
USER_MENU_LINKS = [] USER_MENU_LINKS = []
if SHOW_OPENRC_FILE: if SHOW_OPENRC_FILE:

View File

@ -52,6 +52,7 @@ def data(TEST):
TEST.images_api = utils.TestDataContainer() TEST.images_api = utils.TestDataContainer()
TEST.snapshots = utils.TestDataContainer() TEST.snapshots = utils.TestDataContainer()
TEST.metadata_defs = utils.TestDataContainer() TEST.metadata_defs = utils.TestDataContainer()
TEST.image_schemas = utils.TestDataContainer()
TEST.imagesV2 = utils.TestDataContainer() TEST.imagesV2 = utils.TestDataContainer()
TEST.snapshotsV2 = utils.TestDataContainer() TEST.snapshotsV2 = utils.TestDataContainer()
@ -479,3 +480,148 @@ def data(TEST):
} }
metadef = Namespace(metadef_dict) metadef = Namespace(metadef_dict)
TEST.metadata_defs.add(metadef) TEST.metadata_defs.add(metadef)
image_schemas_dict = {
'additionalProperties': {'type': 'string'},
'links': [
{'href': '{self}', 'rel': 'self'},
{'href': '{file}', 'rel': 'enclosure'},
{'href': '{schema}', 'rel': 'describedby'}
],
'name': 'image',
'properties': {
'architecture': {
'is_base': False,
'type': 'string'
},
'checksum': {
'maxLength': 32,
'readOnly': True,
'type': ['null', 'string']
},
'container_format': {
'enum': [
None,
'ami',
'ari',
'aki',
'bare',
'ovf',
'ova',
'docker',
'compressed'
],
'type': ['null', 'string']
},
'created_at': {'readOnly': True, 'type': 'string'},
'direct_url': {'readOnly': True, 'type': 'string'},
'disk_format': {
'enum': [None, 'raw', 'qcow2'],
'type': ['null', 'string']
},
'file': {'readOnly': True, 'type': 'string'},
'id': {'type': 'string'},
'instance_uuid': {'is_base': False, 'type': 'string'},
'kernel_id': {
'is_base': False,
'type': ['null', 'string']
},
'locations': {
'items': {
'properties': {
'metadata': {'type': 'object'},
'url': {'maxLength': 255, 'type': 'string'},
'validation_data': {
'additionalProperties': False,
'properties': {
'checksum': {
'maxLength': 32,
'minLength': 32,
'type': 'string'
},
'os_hash_algo': {
'maxLength': 64,
'type': 'string'
},
'os_hash_value': {
'maxLength': 128,
'type': 'string'
}
},
'required': ['os_hash_algo', 'os_hash_value'],
'type': 'object',
'writeOnly': True
}
},
'required': ['url', 'metadata'],
'type': 'object'
},
'type': 'array'
},
'min_disk': {'type': 'integer'},
'min_ram': {'type': 'integer'},
'name': {'maxLength': 255, 'type': ['null', 'string']},
'os_distro': {'is_base': False, 'type': 'string'},
'os_hash_algo': {
'maxLength': 64,
'readOnly': True,
'type': ['null', 'string']
},
'os_hash_value': {
'maxLength': 128,
'readOnly': True,
'type': ['null', 'string']
},
'os_hidden': {'type': 'boolean'},
'os_version': {'is_base': False, 'type': 'string'},
'owner': {
'description': 'Owner of the image',
'maxLength': 255,
'type': ['null', 'string']
},
'protected': {'type': 'boolean'},
'ramdisk_id': {
'is_base': False,
'type': ['null', 'string']
},
'schema': {'readOnly': True, 'type': 'string'},
'self': {'readOnly': True, 'type': 'string'},
'size': {'readOnly': True, 'type': ['null', 'integer']},
'status': {
'enum': [
'queued',
'saving',
'active',
'killed',
'deleted',
'uploading',
'importing',
'pending_delete',
'deactivated'
],
'readOnly': True,
'type': 'string'
},
'stores': {'readOnly': True, 'type': 'string'},
'tags': {
'items': {'maxLength': 255, 'type': 'string'},
'type': 'array'
},
'updated_at': {'readOnly': True, 'type': 'string'},
'virtual_size': {
'readOnly': True,
'type': ['null', 'integer']
},
'visibility': {
'enum': [
'community',
'public',
'private',
'shared'
],
'type': 'string'
}
}
}
schemas = Namespace(image_schemas_dict)
TEST.image_schemas.add(schemas)

View File

@ -12,15 +12,18 @@
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
from openstack_dashboard.api.rest import config import mock
from openstack_dashboard import api
from openstack_dashboard.test import helpers as test from openstack_dashboard.test import helpers as test
class ConfigRestTestCase(test.TestCase): class ConfigRestTestCase(test.TestCase):
def test_settings_config_get(self): @mock.patch.object(api.glance, 'get_image_schemas')
def test_settings_config_get(self, mock_schemas_list):
request = self.mock_rest_request() request = self.mock_rest_request()
response = config.Settings().get(request) response = api.rest.config.Settings().get(request)
self.assertStatusCode(response, 200) self.assertStatusCode(response, 200)
self.assertIn(b"REST_API_SETTING_1", response.content) self.assertIn(b"REST_API_SETTING_1", response.content)
self.assertIn(b"REST_API_SETTING_2", response.content) self.assertIn(b"REST_API_SETTING_2", response.content)

View File

@ -314,6 +314,19 @@ class GlanceApiTests(test.APIMockTestCase):
mock_images_get.assert_called_once_with('empty') mock_images_get.assert_called_once_with('empty')
self.assertIsNone(image.name) self.assertIsNone(image.name)
@mock.patch.object(api.glance, 'glanceclient')
def test_get_image_formats(self, mock_glanceclient):
glance_schemas = self.image_schemas.first()
glanceclient = mock_glanceclient.return_value
mock_schemas_list = glanceclient.schemas.get('image').raw()
mock_schemas_list.return_value = glance_schemas
disk_formats = [
item
for item in glance_schemas['properties']['disk_format']['enum']
if item
]
self.assertListEqual(sorted(disk_formats), sorted(['raw', 'qcow2']))
@mock.patch.object(api.glance, 'glanceclient') @mock.patch.object(api.glance, 'glanceclient')
def test_metadefs_namespace_list(self, mock_glanceclient): def test_metadefs_namespace_list(self, mock_glanceclient):
metadata_defs = self.metadata_defs.list() metadata_defs = self.metadata_defs.list()

View File

@ -0,0 +1,6 @@
---
features:
- |
Added support to retrieve supported disk formats from glance,
so you can adjust disk_formats only inside glance-api.conf.
You still can use IMAGE_BACKEND_SETTINGS to adjust format naming.