From e6c03e15e8caa957716eabfee047240622ac3bf5 Mon Sep 17 00:00:00 2001 From: Lucian Petrut Date: Tue, 1 Apr 2014 21:55:16 +0300 Subject: [PATCH] Fixes cinder volume from image on Windows When creating a volume from an image, after the volume is created, qemu-img is supposed to copy the downloaded image to the volume. This fails as the disk is enabled and is not accessible. Also, Windows does not support using dynamic vhds as iSCSI disks and as qemu-img cannot create fixed vhd images, there must be an intermediate conversion before importing the disk. In order to be able to modify an iSCSI disk, it must be disabled first. Closes-Bug: #1300906 Change-Id: I726b23287c5b710227288d6215538f01dfb0f979 --- cinder/image/image_utils.py | 8 +- cinder/tests/test_windows.py | 31 ++++- cinder/tests/test_windows_utils.py | 121 ++++++++++++++++++ cinder/volume/drivers/windows/constants.py | 20 +++ cinder/volume/drivers/windows/windows.py | 22 +++- .../volume/drivers/windows/windows_utils.py | 81 ++++++++++++ 6 files changed, 274 insertions(+), 9 deletions(-) create mode 100644 cinder/tests/test_windows_utils.py create mode 100644 cinder/volume/drivers/windows/constants.py diff --git a/cinder/image/image_utils.py b/cinder/image/image_utils.py index 4e7de692e..b650e5417 100644 --- a/cinder/image/image_utils.py +++ b/cinder/image/image_utils.py @@ -309,8 +309,8 @@ def coalesce_vhd(vhd_path): 'vhd-util', 'coalesce', '-n', vhd_path) -def create_temporary_file(): - fd, tmp = tempfile.mkstemp(dir=CONF.image_conversion_dir) +def create_temporary_file(*args, **kwargs): + fd, tmp = tempfile.mkstemp(dir=CONF.image_conversion_dir, *args, **kwargs) os.close(fd) return tmp @@ -320,9 +320,9 @@ def rename_file(src, dst): @contextlib.contextmanager -def temporary_file(): +def temporary_file(*args, **kwargs): try: - tmp = create_temporary_file() + tmp = create_temporary_file(*args, **kwargs) yield tmp finally: fileutils.delete_if_exists(tmp) diff --git a/cinder/tests/test_windows.py b/cinder/tests/test_windows.py index 8568eef99..99cd3cbe9 100644 --- a/cinder/tests/test_windows.py +++ b/cinder/tests/test_windows.py @@ -32,6 +32,7 @@ from cinder.image import image_utils from cinder.tests.windows import db_fakes from cinder.volume import configuration as conf +from cinder.volume.drivers.windows import constants from cinder.volume.drivers.windows import windows from cinder.volume.drivers.windows import windows_utils @@ -258,10 +259,38 @@ class TestWindowsDriver(test.TestCase): self.stubs.Set(drv, 'local_path', self.fake_local_path) + self.mox.StubOutWithMock(os, 'makedirs') + self.mox.StubOutWithMock(os, 'unlink') + self.mox.StubOutWithMock(image_utils, 'create_temporary_file') self.mox.StubOutWithMock(image_utils, 'fetch_to_vhd') + self.mox.StubOutWithMock(windows_utils.WindowsUtils, 'convert_vhd') + self.mox.StubOutWithMock(windows_utils.WindowsUtils, 'resize_vhd') + self.mox.StubOutWithMock(windows_utils.WindowsUtils, + 'change_disk_status') + + fake_temp_path = r'C:\fake\temp\file' + if (CONF.image_conversion_dir and not + os.path.exists(CONF.image_conversion_dir)): + os.makedirs(CONF.image_conversion_dir) + image_utils.create_temporary_file(suffix='.vhd').AndReturn( + fake_temp_path) + + fake_volume_path = self.fake_local_path(volume) + image_utils.fetch_to_vhd(None, None, None, - self.fake_local_path(volume), + fake_temp_path, mox.IgnoreArg()) + windows_utils.WindowsUtils.change_disk_status(volume['name'], + mox.IsA(bool)) + os.unlink(mox.IsA(str)) + windows_utils.WindowsUtils.convert_vhd(fake_temp_path, + fake_volume_path, + constants.VHD_TYPE_FIXED) + windows_utils.WindowsUtils.resize_vhd(fake_volume_path, + volume['size'] << 30) + windows_utils.WindowsUtils.change_disk_status(volume['name'], + mox.IsA(bool)) + os.unlink(mox.IsA(str)) self.mox.ReplayAll() diff --git a/cinder/tests/test_windows_utils.py b/cinder/tests/test_windows_utils.py new file mode 100644 index 000000000..7cee500af --- /dev/null +++ b/cinder/tests/test_windows_utils.py @@ -0,0 +1,121 @@ +# Copyright 2014 Cloudbase Solutions Srl +# +# 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 cinder import exception +from cinder import test +from cinder.volume.drivers.windows import constants +from cinder.volume.drivers.windows import windows_utils + + +class WindowsUtilsTestCase(test.TestCase): + + _FAKE_FORMAT = 2 + _FAKE_TYPE = 3 + _FAKE_JOB_PATH = 'fake_job_path' + _FAKE_VHD_PATH = r'C:\fake\vhd.vhd' + _FAKE_DESTINATION_PATH = r'C:\fake\destination.vhd' + _FAKE_RET_VAL = 0 + _FAKE_RET_VAL_ERROR = 10 + _FAKE_VHD_SIZE = 1024 + _FAKE_JOB = 'fake_job' + + def setUp(self): + super(WindowsUtilsTestCase, self).setUp() + windows_utils.WindowsUtils.__init__ = lambda x: None + self.wutils = windows_utils.WindowsUtils() + self.wutils._conn_virt = mock.MagicMock() + self.wutils.time = mock.MagicMock() + + def test_convert_vhd(self): + self.wutils.check_ret_val = mock.MagicMock() + mock_img_svc = self.wutils._conn_virt.Msvm_ImageManagementService()[0] + mock_img_svc.ConvertVirtualHardDisk.return_value = ( + self._FAKE_JOB_PATH, self._FAKE_RET_VAL) + + self.wutils.convert_vhd(self._FAKE_VHD_PATH, + self._FAKE_DESTINATION_PATH, + self._FAKE_TYPE) + + mock_img_svc.ConvertVirtualHardDisk.assert_called_once() + self.wutils.check_ret_val.assert_called_once_with( + self._FAKE_RET_VAL, self._FAKE_JOB_PATH) + + def test_resize_vhd(self): + self.wutils.check_ret_val = mock.MagicMock() + mock_img_svc = self.wutils._conn_virt.Msvm_ImageManagementService()[0] + mock_img_svc.ExpandVirtualHardDisk.return_value = (self._FAKE_JOB_PATH, + self._FAKE_RET_VAL) + + self.wutils.resize_vhd(self._FAKE_VHD_PATH, + self._FAKE_VHD_SIZE) + + mock_img_svc.ExpandVirtualHardDisk.assert_called_once() + self.wutils.check_ret_val.assert_called_once_with(self._FAKE_RET_VAL, + self._FAKE_JOB_PATH) + + def _test_check_ret_val(self, job_started, job_failed): + self.wutils._wait_for_job = mock.Mock(return_value=self._FAKE_JOB) + if job_started: + ret_val = self.wutils.check_ret_val( + constants.WMI_JOB_STATUS_STARTED, self._FAKE_JOB_PATH) + self.assertEqual(ret_val, self._FAKE_JOB) + self.wutils._wait_for_job.assert_called_once_with( + self._FAKE_JOB_PATH) + + elif job_failed: + self.assertRaises(exception.VolumeBackendAPIException, + self.wutils.check_ret_val, + 10, self._FAKE_JOB_PATH) + + def test_check_ret_val_failed_job(self): + self._test_check_ret_val(False, True) + + def test_check_ret_val_job_started(self): + self._test_check_ret_val(True, False) + + def _test_wait_for_job(self, job_running, job_failed): + fake_job = mock.MagicMock() + fake_job2 = mock.MagicMock() + fake_job2.JobState = constants.WMI_JOB_STATE_COMPLETED + + if job_running: + fake_job.JobState = constants.WMI_JOB_STATE_RUNNING + elif job_failed: + fake_job.JobState = self._FAKE_RET_VAL_ERROR + fake_job.GetError = mock.Mock(return_value=( + 1, self._FAKE_RET_VAL_ERROR)) + else: + fake_job.JobState = constants.WMI_JOB_STATE_COMPLETED + + self.wutils._get_wmi_obj = mock.Mock(side_effect=[fake_job, fake_job2]) + + if job_failed: + self.assertRaises(exception.VolumeBackendAPIException, + self.wutils._wait_for_job, + self._FAKE_JOB_PATH) + else: + self.wutils._wait_for_job(self._FAKE_JOB_PATH) + if job_running: + call_count = 2 + else: + call_count = 1 + self.assertEqual(call_count, self.wutils._get_wmi_obj.call_count) + + def test_wait_for_running_job(self): + self._test_wait_for_job(True, False) + + def test_wait_for_failed_job(self): + self._test_wait_for_job(False, True) diff --git a/cinder/volume/drivers/windows/constants.py b/cinder/volume/drivers/windows/constants.py new file mode 100644 index 000000000..4c964d93d --- /dev/null +++ b/cinder/volume/drivers/windows/constants.py @@ -0,0 +1,20 @@ +# Copyright 2014 Cloudbase Solutions Srl +# +# 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. + +WMI_JOB_STATUS_STARTED = 4096 +WMI_JOB_STATE_RUNNING = 4 +WMI_JOB_STATE_COMPLETED = 7 + +VHD_TYPE_FIXED = 2 +VHD_TYPE_DYNAMIC = 3 diff --git a/cinder/volume/drivers/windows/windows.py b/cinder/volume/drivers/windows/windows.py index 6821bd32e..fbafb85fb 100644 --- a/cinder/volume/drivers/windows/windows.py +++ b/cinder/volume/drivers/windows/windows.py @@ -26,6 +26,7 @@ from oslo.config import cfg from cinder.image import image_utils from cinder.openstack.common import log as logging from cinder.volume import driver +from cinder.volume.drivers.windows import constants from cinder.volume.drivers.windows import windows_utils LOG = logging.getLogger(__name__) @@ -165,11 +166,24 @@ class WindowsDriver(driver.ISCSIDriver): self.utils.remove_iscsi_target(target_name) def copy_image_to_volume(self, context, volume, image_service, image_id): - """Fetch the image from image_service and write it to the volume.""" + """Fetch the image from image_service and create a volume using it.""" # Convert to VHD and file back to VHD - image_utils.fetch_to_vhd(context, image_service, image_id, - self.local_path(volume), - self.configuration.volume_dd_blocksize) + if (CONF.image_conversion_dir and not + os.path.exists(CONF.image_conversion_dir)): + os.makedirs(CONF.image_conversion_dir) + with image_utils.temporary_file(suffix='.vhd') as tmp: + volume_path = self.local_path(volume) + image_utils.fetch_to_vhd(context, image_service, image_id, tmp, + self.configuration.volume_dd_blocksize) + # The vhd must be disabled and deleted before being replaced with + # the desired image. + self.utils.change_disk_status(volume['name'], False) + os.unlink(volume_path) + self.utils.convert_vhd(tmp, volume_path, + constants.VHD_TYPE_FIXED) + self.utils.resize_vhd(volume_path, + volume['size'] << 30) + self.utils.change_disk_status(volume['name'], True) def copy_volume_to_image(self, context, volume, image_service, image_meta): """Copy the volume to the specified image.""" diff --git a/cinder/volume/drivers/windows/windows_utils.py b/cinder/volume/drivers/windows/windows_utils.py index cd522085e..d99958384 100644 --- a/cinder/volume/drivers/windows/windows_utils.py +++ b/cinder/volume/drivers/windows/windows_utils.py @@ -17,9 +17,11 @@ Utility class for Windows Storage Server 2012 volume related operations. """ import os +import time from cinder import exception from cinder.openstack.common import log as logging +from cinder.volume.drivers.windows import constants # Check needed for unit testing on Unix if os.name == 'nt': @@ -35,6 +37,7 @@ class WindowsUtils(object): # Set the flags self._conn_wmi = wmi.WMI(moniker='//./root/wmi') self._conn_cimv2 = wmi.WMI(moniker='//./root/cimv2') + self._conn_virt = wmi.WMI(moniker='//./root/virtualization') def check_for_setup_error(self): """Check that the driver is working and can communicate. @@ -145,6 +148,19 @@ class WindowsUtils(object): LOG.error(err_msg) raise exception.VolumeBackendAPIException(data=err_msg) + def change_disk_status(self, vol_name, enabled): + try: + cl = self._conn_wmi.WT_Disk(Description=vol_name)[0] + cl.Enabled = enabled + cl.put() + except wmi.x_wmi as exc: + err_msg = (_( + 'Error changing disk status: ' + '%(vol_name)s . WMI exception: ' + '%(wmi_exc)s') % {'vol_name': vol_name, 'wmi_exc': exc}) + LOG.error(err_msg) + raise exception.VolumeBackendAPIException(data=err_msg) + def delete_volume(self, vol_name, vhd_path): """Driver entry point for destroying existing volumes.""" try: @@ -305,3 +321,68 @@ class WindowsUtils(object): 'wmi_exc': exc}) LOG.error(err_msg) raise exception.VolumeBackendAPIException(data=err_msg) + + def convert_vhd(self, src, dest, vhd_type): + # Due to the fact that qemu does not fully support vhdx format yet, + # we must use WMI make conversions between vhd and vhdx formats + image_man_svc = self._conn_virt.Msvm_ImageManagementService()[0] + (job_path, ret_val) = image_man_svc.ConvertVirtualHardDisk( + SourcePath=src, DestinationPath=dest, Type=vhd_type) + self.check_ret_val(ret_val, job_path) + + def resize_vhd(self, vhd_path, new_max_size): + image_man_svc = self._conn_virt.Msvm_ImageManagementService()[0] + (job_path, ret_val) = image_man_svc.ExpandVirtualHardDisk( + Path=vhd_path, MaxInternalSize=new_max_size) + self.check_ret_val(ret_val, job_path) + + def check_ret_val(self, ret_val, job_path, success_values=[0]): + if ret_val == constants.WMI_JOB_STATUS_STARTED: + return self._wait_for_job(job_path) + elif ret_val not in success_values: + raise exception.VolumeBackendAPIException( + _('Operation failed with return value: %s') % ret_val) + + def _wait_for_job(self, job_path): + """Poll WMI job state and wait for completion.""" + job = self._get_wmi_obj(job_path) + + while job.JobState == constants.WMI_JOB_STATE_RUNNING: + time.sleep(0.1) + job = self._get_wmi_obj(job_path) + if job.JobState != constants.WMI_JOB_STATE_COMPLETED: + job_state = job.JobState + if job.path().Class == "Msvm_ConcreteJob": + err_sum_desc = job.ErrorSummaryDescription + err_desc = job.ErrorDescription + err_code = job.ErrorCode + raise exception.VolumeBackendAPIException( + _("WMI job failed with status " + "%(job_state)d. Error details: " + "%(err_sum_desc)s - %(err_desc)s - " + "Error code: %(err_code)d") % + {'job_state': job_state, + 'err_sum_desc': err_sum_desc, + 'err_desc': err_desc, + 'err_code': err_code}) + else: + (error, ret_val) = job.GetError() + if not ret_val and error: + raise exception.VolumeBackendAPIException( + _("WMI job failed with status %(job_state)d. " + "Job path: %(job_path)s Error details: " + "%(error)s") % {'job_state': job_state, + 'error': error, + 'job_path': job_path}) + else: + raise exception.VolumeBackendAPIException( + _("WMI job failed with status %d. No error " + "description available") % job_state) + desc = job.Description + elap = job.ElapsedTime + LOG.debug("WMI job succeeded: %(desc)s, Elapsed=%(elap)s" % + {'desc': desc, 'elap': elap}) + return job + + def _get_wmi_obj(self, path): + return wmi.WMI(moniker=path.replace('\\', '/')) -- 2.45.2