Determine disk_format for volume-backed snapshot from schema
Xen deployments don't support qcow2 images which is what the glance v2 API code in nova defaults to, so basically you can't create a snapshot of a volume-backed instance with glance v2 and Xen right now. This change uses the glance v2 image schema to determine the disk_format to use based on some rules: 1. Look for a preferred disk_format using an ordered list. 2. If we still can't figure it out, just use the first supported disk_format available. Change-Id: Ifaa150fda393e2b49114e30dd5e30e5bf52b4ed1 Closes-Bug: #1616938
This commit is contained in:
+50
-2
@@ -43,6 +43,7 @@ from nova import exception
|
||||
from nova.i18n import _LE, _LI, _LW
|
||||
import nova.image.download as image_xfers
|
||||
from nova import objects
|
||||
from nova.objects import fields
|
||||
from nova import signature_utils
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
@@ -171,7 +172,9 @@ class GlanceClientWrapper(object):
|
||||
client = self.client or self._create_onetime_client(context,
|
||||
version)
|
||||
try:
|
||||
result = getattr(client.images, method)(*args, **kwargs)
|
||||
controller = getattr(client,
|
||||
kwargs.pop('controller', 'images'))
|
||||
result = getattr(controller, method)(*args, **kwargs)
|
||||
if inspect.isgenerator(result):
|
||||
# Convert generator results to a list, so that we can
|
||||
# catch any potential exceptions now and retry the call.
|
||||
@@ -641,6 +644,49 @@ class GlanceImageServiceV2(object):
|
||||
self._client.call(context, 2, 'upload', image_id, data)
|
||||
return self._client.call(context, 2, 'get', image_id)
|
||||
|
||||
def _get_image_create_disk_format_default(self, context):
|
||||
"""Gets an acceptable default image disk_format based on the schema.
|
||||
"""
|
||||
# These preferred disk formats are in order:
|
||||
# 1. we want qcow2 if possible (at least for backward compat)
|
||||
# 2. vhd for xenapi and hyperv
|
||||
# 3. vmdk for vmware
|
||||
# 4. raw should be universally accepted
|
||||
preferred_disk_formats = (
|
||||
fields.DiskFormat.QCOW2,
|
||||
fields.DiskFormat.VHD,
|
||||
fields.DiskFormat.VMDK,
|
||||
fields.DiskFormat.RAW,
|
||||
)
|
||||
|
||||
# Get the image schema - note we don't cache this value since it could
|
||||
# change under us. This looks a bit funky, but what it's basically
|
||||
# doing is calling glanceclient.v2.Client.schemas.get('image').
|
||||
image_schema = self._client.call(context, 2, 'get', 'image',
|
||||
controller='schemas')
|
||||
# get the disk_format schema property from the raw schema
|
||||
disk_format_schema = (
|
||||
image_schema.raw()['properties'].get('disk_format') if image_schema
|
||||
else {}
|
||||
)
|
||||
if disk_format_schema and 'enum' in disk_format_schema:
|
||||
supported_disk_formats = disk_format_schema['enum']
|
||||
# try a priority ordered list
|
||||
for preferred_format in preferred_disk_formats:
|
||||
if preferred_format in supported_disk_formats:
|
||||
return preferred_format
|
||||
# alright, let's just return whatever is available
|
||||
LOG.debug('Unable to find a preferred disk_format for image '
|
||||
'creation with the Image Service v2 API. Using: %s',
|
||||
supported_disk_formats[0])
|
||||
return supported_disk_formats[0]
|
||||
|
||||
LOG.warning(_LW('Unable to determine disk_format schema from the '
|
||||
'Image Service v2 API. Defaulting to '
|
||||
'%(preferred_disk_format)s.'),
|
||||
{'preferred_disk_format': preferred_disk_formats[0]})
|
||||
return preferred_disk_formats[0]
|
||||
|
||||
def _create_v2(self, context, sent_service_image_meta, data=None,
|
||||
force_activate=False):
|
||||
# Glance v1 allows image activation without setting disk and
|
||||
@@ -649,7 +695,9 @@ class GlanceImageServiceV2(object):
|
||||
if force_activate:
|
||||
data = ''
|
||||
if 'disk_format' not in sent_service_image_meta:
|
||||
sent_service_image_meta['disk_format'] = 'qcow2'
|
||||
sent_service_image_meta['disk_format'] = (
|
||||
self._get_image_create_disk_format_default(context)
|
||||
)
|
||||
if 'container_format' not in sent_service_image_meta:
|
||||
sent_service_image_meta['container_format'] = 'bare'
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
|
||||
|
||||
import collections
|
||||
import copy
|
||||
import datetime
|
||||
|
||||
import cryptography
|
||||
@@ -58,6 +59,9 @@ class FakeSchema(object):
|
||||
def is_base_property(self, prop_name):
|
||||
return prop_name in self.base_props
|
||||
|
||||
def raw(self):
|
||||
return copy.deepcopy(self.raw_schema)
|
||||
|
||||
image_fixtures = {
|
||||
'active_image_v1': {
|
||||
'checksum': 'eb9139e4942121f22bbc2afc0400b2a4',
|
||||
@@ -2017,6 +2021,37 @@ class TestCreate(test.NoDBTestCase):
|
||||
|
||||
self.assertEqual(3, client.call.call_count)
|
||||
|
||||
@mock.patch('nova.image.glance._translate_from_glance')
|
||||
@mock.patch('nova.image.glance._translate_to_glance')
|
||||
def test_create_success_v2_force_activate(
|
||||
self, trans_to_mock, trans_from_mock):
|
||||
"""Tests that creating an image with the v2 API with a size of 0 will
|
||||
trigger a call to set the disk and container formats.
|
||||
"""
|
||||
self.flags(use_glance_v1=False, group='glance')
|
||||
translated = {
|
||||
'name': mock.sentinel.name,
|
||||
}
|
||||
trans_to_mock.return_value = translated
|
||||
trans_from_mock.return_value = mock.sentinel.trans_from
|
||||
# size=0 will trigger force_activate=True
|
||||
image_mock = {'size': 0}
|
||||
client = mock.MagicMock()
|
||||
client.call.return_value = {'id': '123'}
|
||||
ctx = mock.sentinel.ctx
|
||||
service = glance.GlanceImageServiceV2(client)
|
||||
with mock.patch.object(service,
|
||||
'_get_image_create_disk_format_default',
|
||||
return_value='vdi'):
|
||||
image_meta = service.create(ctx, image_mock)
|
||||
trans_to_mock.assert_called_once_with(image_mock)
|
||||
# Verify that the disk_format and container_format kwargs are passed.
|
||||
create_call_kwargs = client.call.call_args_list[0][1]
|
||||
self.assertEqual('vdi', create_call_kwargs['disk_format'])
|
||||
self.assertEqual('bare', create_call_kwargs['container_format'])
|
||||
trans_from_mock.assert_called_once_with({'id': '123'})
|
||||
self.assertEqual(mock.sentinel.trans_from, image_meta)
|
||||
|
||||
@mock.patch('nova.image.glance._translate_from_glance')
|
||||
@mock.patch('nova.image.glance._translate_to_glance')
|
||||
def test_create_success_v2_with_location(
|
||||
@@ -2080,6 +2115,66 @@ class TestCreate(test.NoDBTestCase):
|
||||
trans_to_mock.assert_called_once_with(image_mock)
|
||||
self.assertFalse(trans_from_mock.called)
|
||||
|
||||
def _test_get_image_create_disk_format_default(self,
|
||||
test_schema,
|
||||
expected_disk_format):
|
||||
mock_client = mock.MagicMock()
|
||||
mock_client.call.return_value = test_schema
|
||||
service = glance.GlanceImageServiceV2(mock_client)
|
||||
disk_format = service._get_image_create_disk_format_default(
|
||||
mock.sentinel.ctx)
|
||||
self.assertEqual(expected_disk_format, disk_format)
|
||||
mock_client.call.assert_called_once_with(
|
||||
mock.sentinel.ctx, 2, 'get', 'image', controller='schemas')
|
||||
|
||||
def test_get_image_create_disk_format_default_no_schema(self):
|
||||
"""Tests that if there is no disk_format schema we default to qcow2.
|
||||
"""
|
||||
test_schema = FakeSchema({'properties': {}})
|
||||
self._test_get_image_create_disk_format_default(test_schema, 'qcow2')
|
||||
|
||||
def test_get_image_create_disk_format_default_single_entry(self):
|
||||
"""Tests that if there is only a single supported disk_format then
|
||||
we use that.
|
||||
"""
|
||||
test_schema = FakeSchema({
|
||||
'properties': {
|
||||
'disk_format': {
|
||||
'enum': ['iso'],
|
||||
}
|
||||
}
|
||||
})
|
||||
self._test_get_image_create_disk_format_default(test_schema, 'iso')
|
||||
|
||||
def test_get_image_create_disk_format_default_multiple_entries(self):
|
||||
"""Tests that if there are multiple supported disk_formats we look for
|
||||
one in a preferred order.
|
||||
"""
|
||||
test_schema = FakeSchema({
|
||||
'properties': {
|
||||
'disk_format': {
|
||||
# For this test we want to skip qcow2 since that's primary.
|
||||
'enum': ['vhd', 'raw'],
|
||||
}
|
||||
}
|
||||
})
|
||||
self._test_get_image_create_disk_format_default(test_schema, 'vhd')
|
||||
|
||||
def test_get_image_create_disk_format_default_multiple_entries_no_match(
|
||||
self):
|
||||
"""Tests that if we can't match a supported disk_format to what we
|
||||
prefer then we take the first supported disk_format in the list.
|
||||
"""
|
||||
test_schema = FakeSchema({
|
||||
'properties': {
|
||||
'disk_format': {
|
||||
# For this test we want to skip qcow2 since that's primary.
|
||||
'enum': ['aki', 'ari', 'ami'],
|
||||
}
|
||||
}
|
||||
})
|
||||
self._test_get_image_create_disk_format_default(test_schema, 'aki')
|
||||
|
||||
|
||||
class TestUpdate(test.NoDBTestCase):
|
||||
|
||||
|
||||
Reference in New Issue
Block a user