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:
Matt Riedemann
2016-09-24 11:24:40 -04:00
parent c983a6a2bc
commit 2fe5daeb5f
2 changed files with 145 additions and 2 deletions
+50 -2
View File
@@ -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'
+95
View File
@@ -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):