From 2fe5daeb5fcd2750b0eb083fc5c8bfd3fdb992e2 Mon Sep 17 00:00:00 2001 From: Matt Riedemann Date: Sat, 24 Sep 2016 11:24:40 -0400 Subject: [PATCH] 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 --- nova/image/glance.py | 52 ++++++++++++++- nova/tests/unit/image/test_glance.py | 95 ++++++++++++++++++++++++++++ 2 files changed, 145 insertions(+), 2 deletions(-) diff --git a/nova/image/glance.py b/nova/image/glance.py index 60f27c0ba0..0a6ae5a468 100644 --- a/nova/image/glance.py +++ b/nova/image/glance.py @@ -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' diff --git a/nova/tests/unit/image/test_glance.py b/nova/tests/unit/image/test_glance.py index 6158c48903..91e30db1ee 100644 --- a/nova/tests/unit/image/test_glance.py +++ b/nova/tests/unit/image/test_glance.py @@ -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):