From f7593ded8f2e5b768b868914d211163c8b9508ea Mon Sep 17 00:00:00 2001 From: jianghua wang Date: Fri, 25 Aug 2017 02:11:25 +0000 Subject: [PATCH] XenAPI: define a new image handler to use vdi streaming With the new image handler, it creates an image proxy which will use the vdi streaming function from os-xenapi to remotely export VHD from XenServer(image upload) or import VHD to Xenerver(image download). The existing GlanceStore uses custom functionality to directly manipulate files on-disk, so it has the restriction that SR's type must be file system based: e.g. ext or nfs. The new image handler invokes APIs formally supported by XenServer to export/import VDI remotely, it can support other SR types also e.g. lvm, iscsi, etc. Note: vdi streaming would be supported by XenServer 6.5 or above. The function of image handler depends on os-xenapi 0.3.3 or above, so bump os-xenapi's version to 0.3.3 and also declare depends on the patch which bump version in openstack/requirements. Blueprint: xenapi-image-handler-option-improvement Change-Id: I0ad8e34808401ace9b85e1b937a542f4c4e61690 Depends-On: Ib8bc0f837c55839dc85df1d1f0c76b320b9d97b8 --- .../configuration/hypervisor-xen-api.rst | 35 ++-- lower-constraints.txt | 2 +- nova/conf/xenserver.py | 7 +- .../unit/virt/xenapi/image/test_vdi_stream.py | 149 ++++++++++++++++++ nova/virt/xenapi/image/utils.py | 3 +- nova/virt/xenapi/image/vdi_stream.py | 85 ++++++++++ ...xenapi-image-handler-7628a7221b7323e2.yaml | 18 +++ requirements.txt | 2 +- 8 files changed, 285 insertions(+), 16 deletions(-) create mode 100644 nova/tests/unit/virt/xenapi/image/test_vdi_stream.py create mode 100644 nova/virt/xenapi/image/vdi_stream.py diff --git a/doc/source/admin/configuration/hypervisor-xen-api.rst b/doc/source/admin/configuration/hypervisor-xen-api.rst index 7f9544964c00..c43d50ec4464 100644 --- a/doc/source/admin/configuration/hypervisor-xen-api.rst +++ b/doc/source/admin/configuration/hypervisor-xen-api.rst @@ -440,19 +440,30 @@ attached NFS or any other shared storage): sr_matching_filter = "default-sr:true" -Image upload in TGZ compressed format -------------------------------------- +Use different image handler +--------------------------- -To start uploading ``tgz`` compressed raw disk images to the Image service, -configure ``xenapi_image_upload_handler`` by replacing ``GlanceStore`` with -``VdiThroughDevStore``. +We support three different implementations for glance image handler. You +can choose a specific image handler based on the demand: + +* ``direct_vhd``: This image handler will call XAPI plugins to directly + process the VHD files in XenServer SR(Storage Repository). So this handler + only works when the host's SR type is file system based e.g. ext, nfs. + +* ``vdi_local_dev``: This image handler uploads ``tgz`` compressed raw + disk images to the glance image service. + +* ``vdi_remote_stream``: With this image handler, the image data streams + between XenServer and the glance image service. As it uses the remote + APIs supported by XAPI, this plugin works for all SR types supported by + XenServer. + +``direct_vhd`` is the default image handler. If want to use a different image +handler, you can change the config setting of ``image_handler`` within the +``[xenserver]`` section. For example, the following config setting is to use +``vdi_remote_stream`` as the image handler: .. code-block:: ini - xenapi_image_upload_handler=nova.virt.xenapi.image.vdi_through_dev.VdiThroughDevStore - -As opposed to: - -.. code-block:: ini - - xenapi_image_upload_handler=nova.virt.xenapi.image.glance.GlanceStore + [xenserver] + image_handler=vdi_remote_stream diff --git a/lower-constraints.txt b/lower-constraints.txt index 97a98fe814df..0f4b8f547adf 100644 --- a/lower-constraints.txt +++ b/lower-constraints.txt @@ -72,7 +72,7 @@ os-service-types==1.2.0 os-traits==0.4.0 os-vif==1.7.0 os-win==3.0.0 -os-xenapi==0.3.1 +os-xenapi==0.3.3 osc-lib==1.10.0 oslo.cache==1.26.0 oslo.concurrency==3.26.0 diff --git a/nova/conf/xenserver.py b/nova/conf/xenserver.py index f96664ade48c..58f7af23945e 100644 --- a/nova/conf/xenserver.py +++ b/nova/conf/xenserver.py @@ -472,7 +472,7 @@ GlanceStore. """), cfg.StrOpt('image_handler', default='direct_vhd', - choices=('direct_vhd', 'vdi_local_dev'), + choices=('direct_vhd', 'vdi_local_dev', 'vdi_remote_stream'), help=""" The plugin used to handle image uploads and downloads. @@ -488,6 +488,11 @@ the instance's VDI as a local disk to the VM where the OpenStack Compute service runs in. It uploads the raw disk to glance when creating image; When booting an instance from a glance image, it downloads the image and streams it into the disk which is attached to the compute VM. +* ``vdi_remote_stream``: This plugin implements an image handler which works +as a proxy between glance and XenServer. The VHD streams to XenServer via +a remote import API supplied by XAPI for image download; and for image +upload, the VHD streams from XenServer via a remote export API supplied +by XAPI. This plugin works for all SR types supported by XenServer. """), ] diff --git a/nova/tests/unit/virt/xenapi/image/test_vdi_stream.py b/nova/tests/unit/virt/xenapi/image/test_vdi_stream.py new file mode 100644 index 000000000000..8ef3a440b449 --- /dev/null +++ b/nova/tests/unit/virt/xenapi/image/test_vdi_stream.py @@ -0,0 +1,149 @@ +# Copyright 2017 Citrix System +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import mock + +from os_xenapi.client import exception as xenapi_except +from os_xenapi.client import image + +from nova import context +from nova import exception +from nova.image.api import API as image_api +from nova.tests.unit.virt.xenapi import stubs +from nova.virt.xenapi.image import utils +from nova.virt.xenapi.image import vdi_stream +from nova.virt.xenapi import vm_utils + + +class TestVdiStreamStore(stubs.XenAPITestBaseNoDB): + def setUp(self): + super(TestVdiStreamStore, self).setUp() + self.store = vdi_stream.VdiStreamStore() + + self.flags(connection_url='test_url', + image_compression_level=5, + group='xenserver') + + self.session = mock.Mock() + self.context = context.RequestContext( + 'user', 'project', auth_token='foobar') + self.instance = {'uuid': 'e6ad57c9-115e-4b7d-a872-63cea0ac3cf2', + 'system_metadata': [], + 'auto_disk_config': True, + 'os_type': 'default', + 'xenapi_use_agent': 'true'} + + @mock.patch.object(image_api, 'download', + return_value='fake_data') + @mock.patch.object(utils, 'IterableToFileAdapter', + return_value='fake_stream') + @mock.patch.object(vm_utils, 'safe_find_sr', + return_value='fake_sr_ref') + @mock.patch.object(image, 'stream_to_vdis') + def test_download_image(self, stream_to, find_sr, to_file, download): + self.store.download_image(self.context, self.session, + self.instance, 'fake_image_uuid') + + download.assert_called_once_with(self.context, 'fake_image_uuid') + to_file.assert_called_once_with('fake_data') + find_sr.assert_called_once_with(self.session) + stream_to.assert_called_once_with(self.context, self.session, + self.instance, 'test_url', + 'fake_sr_ref', 'fake_stream') + + @mock.patch.object(image_api, 'download', + return_value='fake_data') + @mock.patch.object(utils, 'IterableToFileAdapter', + return_value='fake_stream') + @mock.patch.object(vm_utils, 'safe_find_sr', + return_value='fake_sr_ref') + @mock.patch.object(image, 'stream_to_vdis', + side_effect=xenapi_except.OsXenApiException) + def test_download_image_exception(self, stream_to, find_sr, to_file, + download): + self.assertRaises(exception.CouldNotFetchImage, + self.store.download_image, + self.context, self.session, + self.instance, 'fake_image_uuid') + + @mock.patch.object(vdi_stream.VdiStreamStore, '_get_metadata', + return_value='fake_meta_data') + @mock.patch.object(image, 'stream_from_vdis', + return_value='fake_data') + @mock.patch.object(utils, 'IterableToFileAdapter', + return_value='fake_stream') + @mock.patch.object(image_api, 'update') + def test_upload_image(self, update, to_file, to_stream, get): + fake_vdi_uuids = ['fake-vdi-uuid'] + self.store.upload_image(self.context, self.session, + self.instance, 'fake_image_uuid', + fake_vdi_uuids) + + get.assert_called_once_with(self.context, self.instance, + 'fake_image_uuid') + to_stream.assert_called_once_with(self.context, self.session, + self.instance, 'test_url', + fake_vdi_uuids, compresslevel=5) + to_file.assert_called_once_with('fake_data') + update.assert_called_once_with(self.context, 'fake_image_uuid', + 'fake_meta_data', data='fake_stream') + + @mock.patch.object(vdi_stream.VdiStreamStore, '_get_metadata') + @mock.patch.object(image, 'stream_from_vdis', + side_effect=xenapi_except.OsXenApiException) + @mock.patch.object(utils, 'IterableToFileAdapter', + return_value='fake_stream') + @mock.patch.object(image_api, 'update') + def test_upload_image_exception(self, update, to_file, to_stream, get): + fake_vdi_uuids = ['fake-vdi-uuid'] + self.assertRaises(exception.CouldNotUploadImage, + self.store.upload_image, + self.context, self.session, + self.instance, 'fake_image_uuid', + fake_vdi_uuids) + + @mock.patch.object(image_api, 'get', + return_value={}) + def test_get_metadata(self, image_get): + expect_metadata = {'disk_format': 'vhd', + 'container_format': 'ovf', + 'auto_disk_config': 'True', + 'os_type': 'default', + 'size': 0} + + result = self.store._get_metadata(self.context, self.instance, + 'fake_image_uuid') + + self.assertEqual(result, expect_metadata) + + @mock.patch.object(image_api, 'get', + return_value={}) + def test_get_metadata_disabled(self, image_get): + # Verify the metadata contains auto_disk_config=disabled, when + # image_auto_disk_config is ""Disabled". + self.instance['system_metadata'] = [ + {"key": "image_auto_disk_config", + "value": "Disabled"}] + + expect_metadata = {'disk_format': 'vhd', + 'container_format': 'ovf', + 'auto_disk_config': 'disabled', + 'os_type': 'default', + 'size': 0} + + result = self.store._get_metadata(self.context, self.instance, + 'fake_image_uuid') + + self.assertEqual(result, expect_metadata) diff --git a/nova/virt/xenapi/image/utils.py b/nova/virt/xenapi/image/utils.py index 58585c479556..dbd03d7ceb34 100644 --- a/nova/virt/xenapi/image/utils.py +++ b/nova/virt/xenapi/image/utils.py @@ -25,7 +25,8 @@ _VDI_FORMAT_RAW = 1 IMAGE_API = image.API() IMAGE_HANDLERS = {'direct_vhd': 'glance.GlanceStore', - 'vdi_local_dev': 'vdi_through_dev.VdiThroughDevStore'} + 'vdi_local_dev': 'vdi_through_dev.VdiThroughDevStore', + 'vdi_remote_stream': 'vdi_stream.VdiStreamStore'} def get_image_handler(handler_name): diff --git a/nova/virt/xenapi/image/vdi_stream.py b/nova/virt/xenapi/image/vdi_stream.py new file mode 100644 index 000000000000..963533d95b2d --- /dev/null +++ b/nova/virt/xenapi/image/vdi_stream.py @@ -0,0 +1,85 @@ +# Copyright 2017 Citrix Systems +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +""" This class will stream image data directly between glance and VDI. +""" + +from os_xenapi.client import exception as xenapi_exception +from os_xenapi.client import image as xenapi_image +from oslo_log import log as logging + +import nova.conf +from nova import exception +from nova import image +from nova import utils as nova_utils +from nova.virt.xenapi.image import utils +from nova.virt.xenapi import vm_utils + +CONF = nova.conf.CONF +LOG = logging.getLogger(__name__) + +IMAGE_API = image.API() + + +class VdiStreamStore(object): + def download_image(self, context, session, instance, image_id): + try: + host_url = CONF.xenserver.connection_url + image_data = IMAGE_API.download(context, image_id) + image_stream = utils.IterableToFileAdapter(image_data) + sr_ref = vm_utils.safe_find_sr(session) + vdis = xenapi_image.stream_to_vdis(context, session, + instance, host_url, + sr_ref, image_stream) + except xenapi_exception.OsXenApiException as e: + LOG.error("Image download failed with exception: %s", e) + raise exception.CouldNotFetchImage(image_id=image_id) + return vdis + + def _get_metadata(self, context, instance, image_id): + metadata = IMAGE_API.get(context, image_id) + metadata['disk_format'] = 'vhd' + metadata['container_format'] = 'ovf' + metadata['auto_disk_config'] = str(instance['auto_disk_config']) + metadata['os_type'] = instance.get('os_type') or ( + CONF.xenserver.default_os_type) + # Set size as zero, so that it will update the size in the end + # based on the uploaded image data. + metadata['size'] = 0 + + # Adjust the auto_disk_config value basing on instance's + # system metadata. + # TODO(mriedem): Consider adding an abstract base class for the + # various image handlers to contain common code like this. + auto_disk = nova_utils.get_auto_disk_config_from_instance(instance) + if nova_utils.is_auto_disk_config_disabled(auto_disk): + metadata['auto_disk_config'] = "disabled" + + return metadata + + def upload_image(self, context, session, instance, image_id, vdi_uuids): + try: + host_url = CONF.xenserver.connection_url + level = vm_utils.get_compression_level() + metadata = self._get_metadata(context, instance, image_id) + image_chunks = xenapi_image.stream_from_vdis( + context, session, instance, host_url, vdi_uuids, + compresslevel=level) + image_stream = utils.IterableToFileAdapter(image_chunks) + IMAGE_API.update(context, image_id, metadata, + data=image_stream) + except xenapi_exception.OsXenApiException as e: + LOG.error("Image upload failed with exception: %s", e) + raise exception.CouldNotUploadImage(image_id=image_id) diff --git a/releasenotes/notes/xenapi-image-handler-7628a7221b7323e2.yaml b/releasenotes/notes/xenapi-image-handler-7628a7221b7323e2.yaml index 31b27463d9b1..bad74286f382 100644 --- a/releasenotes/notes/xenapi-image-handler-7628a7221b7323e2.yaml +++ b/releasenotes/notes/xenapi-image-handler-7628a7221b7323e2.yaml @@ -23,6 +23,24 @@ features: an instance from a glance image, it downloads the image and streams it into the disk which is attached to the compute VM. + * ``vdi_remote_stream`` + + This plugin implements an image proxy in nova compute service. + + For image upload, the proxy will export a data stream for a VDI from + XenServer via the remote API supplied by XAPI; convert the stream + to the image format supported by glance; and upload the image to glance. + + For image download, the proxy downloads an image stream from glance; + extracts the data stream from the image stream; and then remotely + imports the data stream to XenServer's VDI via the remote API supplied + by XAPI. + + Note: Under this implementation, the image data may reside in one or + more pieces of storage of various formats on the host, but the import + and export operations interact with a single, proxied VDI object + independent of the underlying structure. + deprecations: - | The ``image_upload_handler`` option in the ``xenserver`` conf section diff --git a/requirements.txt b/requirements.txt index 5dc08840c476..ac59f6fb46fb 100644 --- a/requirements.txt +++ b/requirements.txt @@ -58,7 +58,7 @@ os-vif!=1.8.0,>=1.7.0 # Apache-2.0 os-win>=3.0.0 # Apache-2.0 castellan>=0.16.0 # Apache-2.0 microversion-parse>=0.2.1 # Apache-2.0 -os-xenapi>=0.3.1 # Apache-2.0 +os-xenapi>=0.3.3 # Apache-2.0 tooz>=1.58.0 # Apache-2.0 cursive>=0.2.1 # Apache-2.0 pypowervm>=1.1.15 # Apache-2.0