'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
@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)
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
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()
--- /dev/null
+# 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)
--- /dev/null
+# 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
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__)
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."""
"""
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':
# 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.
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:
'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('\\', '/'))