diff --git a/oslo_vmware/image_transfer.py b/oslo_vmware/image_transfer.py index 825cbc6..4262d9b 100644 --- a/oslo_vmware/image_transfer.py +++ b/oslo_vmware/image_transfer.py @@ -19,6 +19,7 @@ Functions and classes for image transfer between ESX/VC & image service. import errno import logging +import tarfile from eventlet import event from eventlet import greenthread @@ -28,6 +29,7 @@ from eventlet import timeout from oslo_vmware._i18n import _ from oslo_vmware import constants from oslo_vmware import exceptions +from oslo_vmware import image_util from oslo_vmware.objects import datastore as ds_obj from oslo_vmware import rw_handles from oslo_vmware import vim_util @@ -496,6 +498,19 @@ def download_stream_optimized_data(context, timeout_secs, read_handle, return write_handle.get_imported_vm() +def _get_vmdk_handle(ova_handle): + + with tarfile.open(mode="r|", fileobj=ova_handle) as tar: + vmdk_name = None + for tar_info in tar: + if tar_info and tar_info.name.endswith(".ovf"): + vmdk_name = image_util.get_vmdk_name_from_ovf( + tar.extractfile(tar_info)) + elif vmdk_name and tar_info.name.startswith(vmdk_name): + # Actual file name is .XXXXXXX + return tar.extractfile(tar_info) + + def download_stream_optimized_image(context, timeout_secs, image_service, image_id, **kwargs): """Download stream optimized image from image service to VMware server. @@ -512,13 +527,24 @@ def download_stream_optimized_image(context, timeout_secs, image_service, VimSessionOverLoadException, VimConnectionException, ImageTransferException, ValueError """ - LOG.debug("Downloading image: %s from image service as a stream " - "optimized file.", - image_id) + metadata = image_service.show(context, image_id) + container_format = metadata.get('container_format') + + LOG.debug("Downloading image: %(id)s (container: %(container)s) from image" + " service as a stream optimized file.", + {'id': image_id, + 'container': container_format}) # TODO(vbala) catch specific exceptions raised by download call read_iter = image_service.download(context, image_id) read_handle = rw_handles.ImageReadHandle(read_iter) + + if container_format == 'ova': + read_handle = _get_vmdk_handle(read_handle) + if read_handle is None: + raise exceptions.ImageTransferException( + _("No vmdk found in the OVA image %s.") % image_id) + imported_vm = download_stream_optimized_data(context, timeout_secs, read_handle, **kwargs) diff --git a/oslo_vmware/image_util.py b/oslo_vmware/image_util.py new file mode 100644 index 0000000..46546f0 --- /dev/null +++ b/oslo_vmware/image_util.py @@ -0,0 +1,30 @@ +# Copyright (c) 2016 VMware, Inc. +# 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. + +from lxml import etree # nosec (bandit bug 1582516) + + +def _get_vmdk_name_from_ovf(root): + ns_ovf = "{{{0}}}".format(root.nsmap["ovf"]) + disk = root.find("./{0}DiskSection/{0}Disk".format(ns_ovf)) + file_id = disk.get("{0}fileRef".format(ns_ovf)) + f = root.find('./{0}References/{0}File[@{0}id="{1}"]'.format(ns_ovf, + file_id)) + return f.get("{0}href".format(ns_ovf)) + + +def get_vmdk_name_from_ovf(ovf_handle): + """Get the vmdk name from the given ovf descriptor.""" + return _get_vmdk_name_from_ovf(etree.parse(ovf_handle).getroot()) diff --git a/oslo_vmware/tests/test.ovf b/oslo_vmware/tests/test.ovf new file mode 100644 index 0000000..f96e1b2 --- /dev/null +++ b/oslo_vmware/tests/test.ovf @@ -0,0 +1,136 @@ + + + + + + + + + Virtual disk information + + + + The list of logical networks + + The dvportgroup-81 network + + + + A virtual machine + test + + The kind of installed guest operating system + + + Virtual hardware requirements + + Virtual Hardware Family + 0 + test + vmx-10 + + + hertz * 10^6 + Number of Virtual CPUs + 1 virtual CPU(s) + 1 + 3 + 1 + + + byte * 2^20 + Memory Size + 512MB of memory + 2 + 4 + 512 + + + 1 + IDE Controller + VirtualIDEController 1 + 3 + 5 + + + 0 + IDE Controller + VirtualIDEController 0 + 4 + 5 + + + false + VirtualVideoCard + 5 + 24 + + + + + + + + false + VirtualVMCIDevice + 6 + vmware.vmci + 1 + + + + + 1 + true + CD-ROM 1 + ovf:/file/file1 + 7 + 4 + vmware.cdrom.iso + 15 + + + 0 + Hard Disk 1 + ovf:/disk/vmdisk1 + 8 + 4 + 17 + + + + 7 + true + dvportgroup-81 + E1000 ethernet adapter on "dvportgroup-81" + Ethernet 1 + 9 + E1000 + 10 + + + + + + + + + + + + + + + + + + + + + + + A human-readable annotation + foo + + + \ No newline at end of file diff --git a/oslo_vmware/tests/test_image_transfer.py b/oslo_vmware/tests/test_image_transfer.py index d5a2bfb..ff66a56 100644 --- a/oslo_vmware/tests/test_image_transfer.py +++ b/oslo_vmware/tests/test_image_transfer.py @@ -405,59 +405,148 @@ class ImageTransferUtilityTest(base.TestCase): fake_VmdkWriteHandle.get_imported_vm.assert_called_once_with() + @mock.patch('tarfile.open') + @mock.patch('oslo_vmware.image_util.get_vmdk_name_from_ovf') + def test_get_vmdk_handle(self, get_vmdk_name_from_ovf, tar_open): + + ovf_info = mock.Mock() + ovf_info.name = 'test.ovf' + vmdk_info = mock.Mock() + vmdk_info.name = 'test.vmdk' + tar = mock.Mock() + tar.__iter__ = mock.Mock(return_value=iter([ovf_info, vmdk_info])) + tar.__enter__ = mock.Mock(return_value=tar) + tar.__exit__ = mock.Mock(return_value=None) + tar_open.return_value = tar + + ovf_handle = mock.Mock() + get_vmdk_name_from_ovf.return_value = 'test.vmdk' + vmdk_handle = mock.Mock() + tar.extractfile.side_effect = [ovf_handle, vmdk_handle] + + ova_handle = mock.sentinel.ova_handle + ret = image_transfer._get_vmdk_handle(ova_handle) + + self.assertEqual(vmdk_handle, ret) + tar_open.assert_called_once_with(mode="r|", fileobj=ova_handle) + self.assertEqual([mock.call(ovf_info), mock.call(vmdk_info)], + tar.extractfile.call_args_list) + get_vmdk_name_from_ovf.assert_called_once_with(ovf_handle) + + @mock.patch('tarfile.open') + def test_get_vmdk_handle_with_invalid_ova(self, tar_open): + + tar = mock.Mock() + tar.__iter__ = mock.Mock(return_value=iter([])) + tar.__enter__ = mock.Mock(return_value=tar) + tar.__exit__ = mock.Mock(return_value=None) + tar_open.return_value = tar + + ova_handle = mock.sentinel.ova_handle + ret = image_transfer._get_vmdk_handle(ova_handle) + + self.assertIsNone(ret) + tar_open.assert_called_once_with(mode="r|", fileobj=ova_handle) + self.assertFalse(tar.extractfile.called) + @mock.patch('oslo_vmware.rw_handles.ImageReadHandle') @mock.patch.object(image_transfer, 'download_stream_optimized_data') - def test_download_stream_optimized_image( - self, fake_download_stream_optimized_data, - fake_rw_handles_ImageReadHandle): + @mock.patch.object(image_transfer, '_get_vmdk_handle') + def _test_download_stream_optimized_image( + self, + get_vmdk_handle, + download_stream_optimized_data, + image_read_handle, + container=None, + invalid_ova=False): - context = mock.Mock() - session = mock.Mock() - image_id = mock.Mock() - timeout_secs = 10 - image_size = 1000 - host = '127.0.0.1' - port = 443 - resource_pool = 'rp-1' - vm_folder = 'folder-1' - vm_import_spec = None - - fake_iter = 'fake_iter' image_service = mock.Mock() - image_service.download = mock.Mock() - image_service.download.return_value = fake_iter + if container: + image_service.show.return_value = {'container_format': container} + read_iter = mock.sentinel.read_iter + image_service.download.return_value = read_iter + read_handle = mock.sentinel.read_handle + image_read_handle.return_value = read_handle - fake_ImageReadHandle = 'fake_ImageReadHandle' - fake_rw_handles_ImageReadHandle.return_value = fake_ImageReadHandle + if container == 'ova': + if invalid_ova: + get_vmdk_handle.return_value = None + else: + vmdk_handle = mock.sentinel.vmdk_handle + get_vmdk_handle.return_value = vmdk_handle - image_transfer.download_stream_optimized_image( - context, - timeout_secs, - image_service, - image_id, - session=session, - host=host, - port=port, - resource_pool=resource_pool, - vm_folder=vm_folder, - vm_import_spec=vm_import_spec, - image_size=image_size) + imported_vm = mock.sentinel.imported_vm + download_stream_optimized_data.return_value = imported_vm - image_service.download.assert_called_once_with(context, image_id) + context = mock.sentinel.context + timeout_secs = mock.sentinel.timeout_secs + image_id = mock.sentinel.image_id + session = mock.sentinel.session + image_size = mock.sentinel.image_size + host = mock.sentinel.host + port = mock.sentinel.port + resource_pool = mock.sentinel.port + vm_folder = mock.sentinel.vm_folder + vm_import_spec = mock.sentinel.vm_import_spec - fake_rw_handles_ImageReadHandle.assert_called_once_with(fake_iter) + if container == 'ova' and invalid_ova: + self.assertRaises(exceptions.ImageTransferException, + image_transfer.download_stream_optimized_image, + context, + timeout_secs, + image_service, + image_id, + session=session, + host=host, + port=port, + resource_pool=resource_pool, + vm_folder=vm_folder, + vm_import_spec=vm_import_spec, + image_size=image_size) + else: + ret = image_transfer.download_stream_optimized_image( + context, + timeout_secs, + image_service, + image_id, + session=session, + host=host, + port=port, + resource_pool=resource_pool, + vm_folder=vm_folder, + vm_import_spec=vm_import_spec, + image_size=image_size) - fake_download_stream_optimized_data.assert_called_once_with( - context, - timeout_secs, - fake_ImageReadHandle, - session=session, - host=host, - port=port, - resource_pool=resource_pool, - vm_folder=vm_folder, - vm_import_spec=vm_import_spec, - image_size=image_size) + self.assertEqual(imported_vm, ret) + image_service.show.assert_called_once_with(context, image_id) + image_service.download.assert_called_once_with(context, image_id) + image_read_handle.assert_called_once_with(read_iter) + if container == 'ova': + get_vmdk_handle.assert_called_once_with(read_handle) + exp_read_handle = vmdk_handle + else: + exp_read_handle = read_handle + download_stream_optimized_data.assert_called_once_with( + context, + timeout_secs, + exp_read_handle, + session=session, + host=host, + port=port, + resource_pool=resource_pool, + vm_folder=vm_folder, + vm_import_spec=vm_import_spec, + image_size=image_size) + + def test_download_stream_optimized_image(self): + self._test_download_stream_optimized_image() + + def test_download_stream_optimized_image_ova(self): + self._test_download_stream_optimized_image(container='ova') + + def test_download_stream_optimized_image_invalid_ova(self): + self._test_download_stream_optimized_image(container='ova', + invalid_ova=True) @mock.patch.object(image_transfer, '_start_transfer') @mock.patch('oslo_vmware.rw_handles.VmdkReadHandle') diff --git a/oslo_vmware/tests/test_image_util.py b/oslo_vmware/tests/test_image_util.py new file mode 100644 index 0000000..f97f33f --- /dev/null +++ b/oslo_vmware/tests/test_image_util.py @@ -0,0 +1,32 @@ +# Copyright (c) 2016 VMware, Inc. +# 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. + +""" +Unit tests for image_util. +""" + +import os + +from oslo_vmware import image_util +from oslo_vmware.tests import base + + +class ImageUtilTest(base.TestCase): + + def test_get_vmdk_name_from_ovf(self): + ovf_descriptor = os.path.join(os.path.dirname(__file__), 'test.ovf') + with open(ovf_descriptor) as f: + vmdk_name = image_util.get_vmdk_name_from_ovf(f) + self.assertEqual("test-disk1.vmdk", vmdk_name) diff --git a/requirements.txt b/requirements.txt index 875618d..2752795 100644 --- a/requirements.txt +++ b/requirements.txt @@ -14,6 +14,7 @@ oslo.utils>=3.5.0 # Apache-2.0 # for the routing notifier PyYAML>=3.1.0 # MIT +lxml>=2.3 # BSD suds-jurko>=0.6 # LGPL eventlet!=0.18.3,>=0.18.2 # MIT requests>=2.10.0 # Apache-2.0