]> review.fuel-infra Code Review - openstack-build/cinder-build.git/commitdiff
VMware: Implement retype for VMDK driver
authorVipin Balachandran <vbala@vmware.com>
Fri, 8 Aug 2014 12:50:51 +0000 (18:20 +0530)
committerVipin Balachandran <vbala@vmware.com>
Wed, 17 Sep 2014 06:30:55 +0000 (12:00 +0530)
This patch adds support for retype operation in the VMDK driver.

The VMDK driver uses the extra spec keys 'vmware:vmdk_type' and
'vmware:storage_profile' to determine the disk provisioning type
and storage policy of the backing VM. Currently it is not possible
to change the disk provisioning type or storage policy after volume
creation. With retype support, user can change the disk provisioning
type and storage policy by changing the volume's type to another type
with extra spec keys set to desired values.

Closes-Bug: #1275682
Partial-Bug: #1288254

Change-Id: If4b868af7b91373b2b36fd5cd2f0dda12d604d99

cinder/tests/test_vmware_vmdk.py
cinder/tests/test_vmware_volumeops.py
cinder/volume/drivers/vmware/vmdk.py
cinder/volume/drivers/vmware/volumeops.py

index 85228b10620b0777910cbf16b1d85a61ad1b6dff..b00868ffc1b25827221fb9463244a9227733c2ed 100644 (file)
@@ -29,6 +29,7 @@ from cinder.openstack.common import units
 from cinder import test
 from cinder.volume import configuration
 from cinder.volume.drivers.vmware import api
+from cinder.volume.drivers.vmware import datastore as hub
 from cinder.volume.drivers.vmware import error_util
 from cinder.volume.drivers.vmware import vim
 from cinder.volume.drivers.vmware import vim_util
@@ -254,6 +255,9 @@ class VMwareEsxVmdkDriverTestCase(test.TestCase):
         self.assertRaises(error_util.VimFaultException, driver.create_volume,
                           volume)
 
+        # Clear side effects.
+        driver._select_ds_for_volume.side_effect = None
+
     def test_success_wait_for_task(self):
         """Test successful wait_for_task."""
         m = self.mox
@@ -501,12 +505,29 @@ class VMwareEsxVmdkDriverTestCase(test.TestCase):
         size = volume['size'] * units.Gi
         driver._select_datastore_summary.assert_called_once_with(size, dss)
 
-    def test_get_disk_type(self):
+    @mock.patch('cinder.volume.volume_types.get_volume_type_extra_specs')
+    def test_get_disk_type(self, get_volume_type_extra_specs):
         """Test _get_disk_type."""
-        volume = FakeObject()
-        volume['volume_type_id'] = None
-        self.assertEqual(vmdk.VMwareEsxVmdkDriver._get_disk_type(volume),
-                         'thin')
+        # Test with no volume type.
+        volume = {'volume_type_id': None}
+        self.assertEqual(vmdk.THIN_VMDK_TYPE,
+                         vmdk.VMwareEsxVmdkDriver._get_disk_type(volume))
+
+        # Test with valid vmdk_type.
+        volume_type_id = mock.sentinel.volume_type_id
+        volume = {'volume_type_id': volume_type_id}
+        get_volume_type_extra_specs.return_value = vmdk.THICK_VMDK_TYPE
+
+        self.assertEqual(vmdk.THICK_VMDK_TYPE,
+                         vmdk.VMwareEsxVmdkDriver._get_disk_type(volume))
+        get_volume_type_extra_specs.assert_called_once_with(volume_type_id,
+                                                            'vmware:vmdk_type')
+        # Test with invalid vmdk_type.
+        get_volume_type_extra_specs.return_value = 'sparse'
+
+        self.assertRaises(error_util.InvalidDiskTypeException,
+                          vmdk.VMwareEsxVmdkDriver._get_disk_type,
+                          volume)
 
     def test_init_conn_with_instance_no_backing(self):
         """Test initialize_connection with instance and without backing."""
@@ -1400,6 +1421,163 @@ class VMwareEsxVmdkDriverTestCase(test.TestCase):
         m.UnsetStubs()
         m.VerifyAll()
 
+    @mock.patch.object(VMDK_DRIVER, '_delete_temp_backing')
+    @mock.patch('cinder.openstack.common.uuidutils.generate_uuid')
+    @mock.patch.object(VMDK_DRIVER, '_get_volume_group_folder')
+    @mock.patch('cinder.volume.volume_types.get_volume_type_extra_specs')
+    @mock.patch.object(VMDK_DRIVER, 'volumeops')
+    @mock.patch.object(VMDK_DRIVER, 'ds_sel')
+    def test_retype(self, ds_sel, vops, get_volume_type_extra_specs,
+                    get_volume_group_folder, generate_uuid,
+                    delete_temp_backing):
+        self._test_retype(ds_sel, vops, get_volume_type_extra_specs,
+                          get_volume_group_folder, generate_uuid,
+                          delete_temp_backing)
+
+    def _test_retype(self, ds_sel, vops, get_volume_type_extra_specs,
+                     get_volume_group_folder, genereate_uuid,
+                     delete_temp_backing):
+        self._driver._storage_policy_enabled = True
+        context = mock.sentinel.context
+        diff = mock.sentinel.diff
+        host = mock.sentinel.host
+        new_type = {'id': 'abc'}
+
+        # Test with in-use volume.
+        vol = {'size': 1, 'status': 'retyping', 'name': 'vol-1',
+               'volume_type_id': 'def', 'instance_uuid': '583a8dbb'}
+        self.assertFalse(self._driver.retype(context, vol, new_type, diff,
+                                             host))
+
+        # Test with no backing.
+        vops.get_backing.return_value = None
+        vol['instance_uuid'] = None
+        self.assertTrue(self._driver.retype(context, vol, new_type, diff,
+                                            host))
+
+        # Test with no disk type conversion, no profile change and
+        # compliant datastore.
+        ds_value = mock.sentinel.datastore_value
+        datastore = mock.Mock(value=ds_value)
+        vops.get_datastore.return_value = datastore
+
+        backing = mock.sentinel.backing
+        vops.get_backing.return_value = backing
+
+        get_volume_type_extra_specs.side_effect = [vmdk.THIN_VMDK_TYPE,
+                                                   vmdk.THIN_VMDK_TYPE,
+                                                   None,
+                                                   None]
+        ds_sel.is_datastore_compliant.return_value = True
+        self.assertTrue(self._driver.retype(context, vol, new_type, diff,
+                                            host))
+
+        # Test with no disk type conversion, profile change and
+        # compliant datastore.
+        new_profile = mock.sentinel.new_profile
+        get_volume_type_extra_specs.side_effect = [vmdk.THIN_VMDK_TYPE,
+                                                   vmdk.THIN_VMDK_TYPE,
+                                                   'gold-1',
+                                                   new_profile]
+        ds_sel.is_datastore_compliant.return_value = True
+        profile_id = mock.sentinel.profile_id
+        ds_sel.get_profile_id.return_value = profile_id
+
+        self.assertTrue(self._driver.retype(context, vol, new_type, diff,
+                                            host))
+        vops.change_backing_profile.assert_called_once_with(backing,
+                                                            profile_id)
+
+        # Test with disk type conversion, profile change and a backing with
+        # snapshots. Also test the no candidate datastore case.
+        get_volume_type_extra_specs.side_effect = [vmdk.THICK_VMDK_TYPE,
+                                                   vmdk.THIN_VMDK_TYPE,
+                                                   'gold-1',
+                                                   new_profile]
+        vops.snapshot_exists.return_value = True
+        ds_sel.select_datastore.return_value = ()
+
+        self.assertFalse(self._driver.retype(context, vol, new_type, diff,
+                                             host))
+        exp_req = {hub.DatastoreSelector.HARD_ANTI_AFFINITY_DS: [ds_value],
+                   hub.DatastoreSelector.PROFILE_NAME: new_profile,
+                   hub.DatastoreSelector.SIZE_BYTES: units.Gi}
+        ds_sel.select_datastore.assert_called_once_with(exp_req)
+
+        # Modify the previous case with a candidate datastore which is
+        # different than the backing's current datastore.
+        get_volume_type_extra_specs.side_effect = [vmdk.THICK_VMDK_TYPE,
+                                                   vmdk.THIN_VMDK_TYPE,
+                                                   'gold-1',
+                                                   new_profile]
+        vops.snapshot_exists.return_value = True
+
+        host = mock.sentinel.host
+        rp = mock.sentinel.rp
+        candidate_ds = mock.Mock(value=mock.sentinel.candidate_ds_value)
+        summary = mock.Mock(datastore=candidate_ds)
+        ds_sel.select_datastore.return_value = (host, rp, summary)
+
+        folder = mock.sentinel.folder
+        get_volume_group_folder.return_value = folder
+
+        vops.change_backing_profile.reset_mock()
+
+        self.assertTrue(self._driver.retype(context, vol, new_type, diff,
+                                            host))
+        vops.relocate_backing.assert_called_once_with(
+            backing, candidate_ds, rp, host, vmdk.THIN_VMDK_TYPE)
+        vops.move_backing_to_folder.assert_called_once_with(backing, folder)
+        vops.change_backing_profile.assert_called_once_with(backing,
+                                                            profile_id)
+
+        # Test with disk type conversion, profile change, backing with
+        # no snapshots and candidate datastore which is same as the backing
+        # datastore.
+        get_volume_type_extra_specs.side_effect = [vmdk.THICK_VMDK_TYPE,
+                                                   vmdk.THIN_VMDK_TYPE,
+                                                   'gold-1',
+                                                   new_profile]
+        vops.snapshot_exists.return_value = False
+        summary.datastore = datastore
+
+        uuid = '025b654b-d4ed-47f9-8014-b71a7744eafc'
+        genereate_uuid.return_value = uuid
+
+        clone = mock.sentinel.clone
+        vops.clone_backing.return_value = clone
+
+        vops.change_backing_profile.reset_mock()
+
+        self.assertTrue(self._driver.retype(context, vol, new_type, diff,
+                                            host))
+        vops.rename_backing.assert_called_once_with(backing, uuid)
+        vops.clone_backing.assert_called_once_with(
+            vol['name'], backing, None, volumeops.FULL_CLONE_TYPE,
+            datastore, vmdk.THIN_VMDK_TYPE)
+        delete_temp_backing.assert_called_once_with(backing)
+        vops.change_backing_profile.assert_called_once_with(clone,
+                                                            profile_id)
+
+        # Modify the previous case with exception during clone.
+        get_volume_type_extra_specs.side_effect = [vmdk.THICK_VMDK_TYPE,
+                                                   vmdk.THIN_VMDK_TYPE,
+                                                   'gold-1',
+                                                   new_profile]
+
+        vops.clone_backing.side_effect = error_util.VimException('error')
+
+        vops.rename_backing.reset_mock()
+        vops.change_backing_profile.reset_mock()
+
+        self.assertRaises(
+            error_util.VimException, self._driver.retype, context, vol,
+            new_type, diff, host)
+        exp_rename_calls = [mock.call(backing, uuid),
+                            mock.call(backing, vol['name'])]
+        self.assertEqual(exp_rename_calls, vops.rename_backing.call_args_list)
+        self.assertFalse(vops.change_backing_profile.called)
+
     @mock.patch.object(VMDK_DRIVER, 'volumeops')
     def test_extend_vmdk_virtual_disk(self, volume_ops):
         """Test vmdk._extend_vmdk_virtual_disk."""
@@ -2288,6 +2466,9 @@ class VMwareVcVmdkDriverTestCase(VMwareEsxVmdkDriverTestCase):
         driver._select_datastore_summary.assert_called_once_with(size,
                                                                  filtered_dss)
 
+        # Clear side effects.
+        driver._filter_ds_by_profile.side_effect = None
+
     @mock.patch.object(VMDK_DRIVER, 'volumeops')
     def test_extend_vmdk_virtual_disk(self, volume_ops):
         """Test vmdk._extend_vmdk_virtual_disk."""
@@ -2364,6 +2545,19 @@ class VMwareVcVmdkDriverTestCase(VMwareEsxVmdkDriverTestCase):
                                                          _extend_virtual_disk,
                                                          fetch_optimized_image)
 
+    @mock.patch.object(VMDK_DRIVER, '_delete_temp_backing')
+    @mock.patch('cinder.openstack.common.uuidutils.generate_uuid')
+    @mock.patch.object(VMDK_DRIVER, '_get_volume_group_folder')
+    @mock.patch('cinder.volume.volume_types.get_volume_type_extra_specs')
+    @mock.patch.object(VMDK_DRIVER, 'volumeops')
+    @mock.patch.object(VMDK_DRIVER, 'ds_sel')
+    def test_retype(self, ds_sel, vops, get_volume_type_extra_specs,
+                    get_volume_group_folder, generate_uuid,
+                    delete_temp_backing):
+        self._test_retype(ds_sel, vops, get_volume_type_extra_specs,
+                          get_volume_group_folder, generate_uuid,
+                          delete_temp_backing)
+
     @mock.patch.object(VMDK_DRIVER, '_select_ds_for_volume')
     @mock.patch.object(VMDK_DRIVER, '_extend_vmdk_virtual_disk')
     @mock.patch.object(VMDK_DRIVER, 'volumeops')
index caaeb0fd5ce7cfee6cde2b6e0b6d7f760189afb9..0817fb865ab5404543eebd1c8958dd775103568d 100644 (file)
@@ -688,22 +688,33 @@ class VolumeOpsTestCase(test.TestCase):
         self.assertEqual('', backing.fileName)
         self.assertEqual('persistent', backing.diskMode)
 
+    @mock.patch('cinder.volume.drivers.vmware.volumeops.VMwareVolumeOps.'
+                '_get_disk_device')
     @mock.patch('cinder.volume.drivers.vmware.volumeops.VMwareVolumeOps.'
                 '_get_relocate_spec')
-    def test_relocate_backing(self, get_relocate_spec):
+    def test_relocate_backing(self, get_relocate_spec, get_disk_device):
+        disk_device = mock.sentinel.disk_device
+        get_disk_device.return_value = disk_device
+
         spec = mock.sentinel.relocate_spec
         get_relocate_spec.return_value = spec
+
         task = mock.sentinel.task
         self.session.invoke_api.return_value = task
+
         backing = mock.sentinel.backing
         datastore = mock.sentinel.datastore
         resource_pool = mock.sentinel.resource_pool
         host = mock.sentinel.host
-        self.vops.relocate_backing(backing, datastore, resource_pool, host)
+        disk_type = mock.sentinel.disk_type
+        self.vops.relocate_backing(backing, datastore, resource_pool, host,
+                                   disk_type)
         # Verify calls
         disk_move_type = 'moveAllDiskBackingsAndAllowSharing'
+        get_disk_device.assert_called_once_with(backing)
         get_relocate_spec.assert_called_once_with(datastore, resource_pool,
-                                                  host, disk_move_type)
+                                                  host, disk_move_type,
+                                                  disk_type, disk_device)
         self.session.invoke_api.assert_called_once_with(self.session.vim,
                                                         'RelocateVM_Task',
                                                         backing,
@@ -1001,6 +1012,50 @@ class VolumeOpsTestCase(test.TestCase):
                                                         newName=new_name)
         self.session.wait_for_task.assert_called_once_with(task)
 
+    def test_change_backing_profile(self):
+        # Test change to empty profile.
+        reconfig_spec = mock.Mock()
+        empty_profile_spec = mock.sentinel.empty_profile_spec
+        self.session.vim.client.factory.create.side_effect = [
+            reconfig_spec, empty_profile_spec]
+
+        task = mock.sentinel.task
+        self.session.invoke_api.return_value = task
+
+        backing = mock.sentinel.backing
+        unique_profile_id = mock.sentinel.unique_profile_id
+        profile_id = mock.Mock(uniqueId=unique_profile_id)
+        self.vops.change_backing_profile(backing, profile_id)
+
+        self.assertEqual([empty_profile_spec], reconfig_spec.vmProfile)
+        self.session.invoke_api.assert_called_once_with(self.session.vim,
+                                                        "ReconfigVM_Task",
+                                                        backing,
+                                                        spec=reconfig_spec)
+        self.session.wait_for_task.assert_called_once_with(task)
+
+        # Test change to non-empty profile.
+        profile_spec = mock.Mock()
+        self.session.vim.client.factory.create.side_effect = [
+            reconfig_spec, profile_spec]
+
+        self.session.invoke_api.reset_mock()
+        self.session.wait_for_task.reset_mock()
+
+        self.vops.change_backing_profile(backing, profile_id)
+
+        self.assertEqual([profile_spec], reconfig_spec.vmProfile)
+        self.assertEqual(unique_profile_id,
+                         reconfig_spec.vmProfile[0].profileId)
+        self.session.invoke_api.assert_called_once_with(self.session.vim,
+                                                        "ReconfigVM_Task",
+                                                        backing,
+                                                        spec=reconfig_spec)
+        self.session.wait_for_task.assert_called_once_with(task)
+
+        # Clear side effects.
+        self.session.vim.client.factory.create.side_effect = None
+
     def test_delete_file(self):
         file_mgr = mock.sentinel.file_manager
         self.session.vim.service_content.fileManager = file_mgr
index d27a9bd244706a916798ce955cb766d24f45bd73..b8f48ec517d3c82a024b4791f4e74e0453d49c05 100644 (file)
@@ -38,6 +38,7 @@ from cinder.openstack.common import units
 from cinder.openstack.common import uuidutils
 from cinder.volume import driver
 from cinder.volume.drivers.vmware import api
+from cinder.volume.drivers.vmware import datastore as hub
 from cinder.volume.drivers.vmware import error_util
 from cinder.volume.drivers.vmware import vim
 from cinder.volume.drivers.vmware import vim_util
@@ -208,6 +209,7 @@ class VMwareEsxVmdkDriver(driver.VolumeDriver):
         # No storage policy based placement possible when connecting
         # directly to ESX
         self._storage_policy_enabled = False
+        self._ds_sel = None
 
     @property
     def session(self):
@@ -232,6 +234,13 @@ class VMwareEsxVmdkDriver(driver.VolumeDriver):
                                                         max_objects)
         return self._volumeops
 
+    @property
+    def ds_sel(self):
+        if not self._ds_sel:
+            self._ds_sel = hub.DatastoreSelector(self.volumeops,
+                                                 self.session)
+        return self._ds_sel
+
     def do_setup(self, context):
         """Perform validations and establish connection to server.
 
@@ -396,6 +405,13 @@ class VMwareEsxVmdkDriver(driver.VolumeDriver):
                   {'datastore': best_summary, 'host_count': max_host_count})
         return best_summary
 
+    def _get_extra_spec_storage_profile(self, type_id):
+        """Get storage profile name in the given volume type's extra spec.
+
+        If there is no storage profile in the extra spec, default is None.
+        """
+        return _get_volume_type_extra_spec(type_id, 'storage_profile')
+
     def _get_storage_profile(self, volume):
         """Get storage profile associated with the given volume's volume_type.
 
@@ -403,10 +419,7 @@ class VMwareEsxVmdkDriver(driver.VolumeDriver):
         :return: String value of storage profile if volume type is associated
                  and contains storage_profile extra_spec option; None otherwise
         """
-        type_id = volume['volume_type_id']
-        if type_id is None:
-            return None
-        return _get_volume_type_extra_spec(type_id, 'storage_profile')
+        return self._get_extra_spec_storage_profile(volume['volume_type_id'])
 
     def _filter_ds_by_profile(self, datastores, storage_profile):
         """Filter out datastores that do not match given storage profile.
@@ -462,18 +475,27 @@ class VMwareEsxVmdkDriver(driver.VolumeDriver):
                                                            datastores)
         return (folder, datastore_summary)
 
+    @staticmethod
+    def _get_extra_spec_disk_type(type_id):
+        """Get disk type from the given volume type's extra spec.
+
+        If there is no disk type option, default is THIN_VMDK_TYPE.
+        """
+        disk_type = _get_volume_type_extra_spec(type_id,
+                                                'vmdk_type',
+                                                default_value=THIN_VMDK_TYPE)
+        volumeops.VirtualDiskType.validate(disk_type)
+        return disk_type
+
     @staticmethod
     def _get_disk_type(volume):
-        """Get disk type from volume type.
+        """Get disk type from the given volume's volume type.
 
         :param volume: Volume object
         :return: Disk type
         """
-        return _get_volume_type_extra_spec(volume['volume_type_id'],
-                                           'vmdk_type',
-                                           (THIN_VMDK_TYPE, THICK_VMDK_TYPE,
-                                            EAGER_ZEROED_THICK_VMDK_TYPE),
-                                           THIN_VMDK_TYPE)
+        return VMwareEsxVmdkDriver._get_extra_spec_disk_type(
+            volume['volume_type_id'])
 
     def _get_storage_profile_id(self, volume):
         storage_profile = self._get_storage_profile(volume)
@@ -1347,6 +1369,173 @@ class VMwareEsxVmdkDriver(driver.VolumeDriver):
         LOG.info(_("Done copying volume %(vol)s to a new image %(img)s") %
                  {'vol': volume['name'], 'img': image_meta['name']})
 
+    def _in_use(self, volume):
+        """Check if the given volume is in use."""
+        return volume['instance_uuid'] is not None
+
+    def retype(self, ctxt, volume, new_type, diff, host):
+        """Convert the volume to be of the new type.
+
+        The retype is performed only if the volume is not in use. Retype is NOP
+        if the backing doesn't exist. If disk type conversion is needed, the
+        volume is cloned. If disk type conversion is needed and the volume
+        contains snapshots, the backing is relocated instead of cloning. The
+        backing is also relocated if the current datastore is not compliant
+        with the new storage profile (if any). Finally, the storage profile of
+        the backing VM is updated.
+
+        :param ctxt: Context
+        :param volume: A dictionary describing the volume to retype
+        :param new_type: A dictionary describing the volume type to convert to
+        :param diff: A dictionary with the difference between the two types
+        :param host: A dictionary describing the host to migrate to, where
+                     host['host'] is its name, and host['capabilities'] is a
+                     dictionary of its reported capabilities (unused)
+        :returns: True if the retype occurred; False otherwise.
+        """
+        # Can't attempt retype if the volume is in use.
+        if self._in_use(volume):
+            LOG.warn(_("Volume: %s is in use, can't retype."),
+                     volume['name'])
+            return False
+
+        # If the backing doesn't exist, retype is NOP.
+        backing = self.volumeops.get_backing(volume['name'])
+        if backing is None:
+            LOG.debug("Backing for volume: %s doesn't exist; retype is NOP.",
+                      volume['name'])
+            return True
+
+        # Check whether we need disk type conversion.
+        disk_type = VMwareEsxVmdkDriver._get_disk_type(volume)
+        new_disk_type = VMwareEsxVmdkDriver._get_extra_spec_disk_type(
+            new_type['id'])
+        need_disk_type_conversion = disk_type != new_disk_type
+
+        # Check whether we need to relocate the backing. If the backing
+        # contains snapshots, relocate is the only way to achieve disk type
+        # conversion.
+        need_relocate = (need_disk_type_conversion and
+                         self.volumeops.snapshot_exists(backing))
+
+        datastore = self.volumeops.get_datastore(backing)
+
+        # Check whether we need to change the storage profile.
+        need_profile_change = False
+        is_compliant = True
+        new_profile = None
+        if self._storage_policy_enabled:
+            profile = self._get_storage_profile(volume)
+            new_profile = self._get_extra_spec_storage_profile(new_type['id'])
+            need_profile_change = profile != new_profile
+            # The current datastore may be compliant with the new profile.
+            is_compliant = self.ds_sel.is_datastore_compliant(datastore,
+                                                              new_profile)
+
+        # No need to relocate or clone if there is no disk type conversion and
+        # the current datastore is compliant with the new profile or storage
+        # policy is disabled.
+        if not need_disk_type_conversion and is_compliant:
+            LOG.debug("Backing: %(backing)s for volume: %(name)s doesn't need "
+                      "disk type conversion.",
+                      {'backing': backing,
+                       'name': volume['name']})
+            if self._storage_policy_enabled:
+                LOG.debug("Backing: %(backing)s for volume: %(name)s is "
+                          "compliant with the new profile: %(new_profile)s.",
+                          {'backing': backing,
+                           'name': volume['name'],
+                           'new_profile': new_profile})
+        else:
+            # Set requirements for datastore selection.
+            req = {}
+            req[hub.DatastoreSelector.SIZE_BYTES] = (volume['size'] *
+                                                     units.Gi)
+
+            if need_relocate:
+                LOG.debug("Backing: %s should be relocated.", backing)
+                req[hub.DatastoreSelector.HARD_ANTI_AFFINITY_DS] = (
+                    [datastore.value])
+
+            if need_profile_change:
+                LOG.debug("Backing: %(backing)s needs a profile change to: "
+                          "%(profile)s.",
+                          {'backing': backing,
+                           'profile': new_profile})
+                req[hub.DatastoreSelector.PROFILE_NAME] = new_profile
+
+            # Select datastore satisfying the requirements.
+            best_candidate = self.ds_sel.select_datastore(req)
+            if not best_candidate:
+                # No candidate datastores; can't retype.
+                LOG.warn(_("There are no datastores matching new requirements;"
+                           " can't retype volume: %s."),
+                         volume['name'])
+                return False
+
+            (host, rp, summary) = best_candidate
+            new_datastore = summary.datastore
+            if datastore.value != new_datastore.value:
+                # Datastore changed; relocate the backing.
+                LOG.debug("Backing: %s needs to be relocated for retype.",
+                          backing)
+                self.volumeops.relocate_backing(
+                    backing, new_datastore, rp, host, new_disk_type)
+
+                dc = self.volumeops.get_dc(rp)
+                folder = self._get_volume_group_folder(dc)
+                self.volumeops.move_backing_to_folder(backing, folder)
+            elif need_disk_type_conversion:
+                # Same datastore, but clone is needed for disk type conversion.
+                LOG.debug("Backing: %s needs to be cloned for retype.",
+                          backing)
+
+                new_backing = None
+                renamed = False
+                tmp_name = uuidutils.generate_uuid()
+                try:
+                    self.volumeops.rename_backing(backing, tmp_name)
+                    renamed = True
+
+                    new_backing = self.volumeops.clone_backing(
+                        volume['name'], backing, None,
+                        volumeops.FULL_CLONE_TYPE, datastore, new_disk_type)
+                    self._delete_temp_backing(backing)
+                    backing = new_backing
+                except error_util.VimException:
+                    with excutils.save_and_reraise_exception():
+                        LOG.exception(_("Error occurred while cloning backing:"
+                                        " %s during retype."),
+                                      backing)
+                        if renamed:
+                            LOG.debug("Undo rename of backing: %(backing)s; "
+                                      "changing name from %(new_name)s to "
+                                      "%(old_name)s.",
+                                      {'backing': backing,
+                                       'new_name': tmp_name,
+                                       'old_name': volume['name']})
+                            try:
+                                self.volumeops.rename_backing(backing,
+                                                              volume['name'])
+                            except error_util.VimException:
+                                LOG.warn(_("Changing backing: %(backing)s name"
+                                           " from %(new_name)s to %(old_name)s"
+                                           " failed."),
+                                         {'backing': backing,
+                                          'new_name': tmp_name,
+                                          'old_name': volume['name']})
+
+        # Update the backing's storage profile if needed.
+        if need_profile_change:
+            profile_id = None
+            if new_profile is not None:
+                profile_id = self.ds_sel.get_profile_id(new_profile)
+            self.volumeops.change_backing_profile(backing, profile_id)
+
+        # Retype is done.
+        LOG.debug("Volume: %s retype is done.", volume['name'])
+        return True
+
     def extend_volume(self, volume, new_size):
         """Extend vmdk to new_size.
 
index ce3b92632e735b82839a226ba462a541f74dd7c4..4fb4a3b12b0ff904bb6c252695e7ff279f2fc1b9 100644 (file)
@@ -831,7 +831,8 @@ class VMwareVolumeOps(object):
         LOG.debug("Spec for relocating the backing: %s.", relocate_spec)
         return relocate_spec
 
-    def relocate_backing(self, backing, datastore, resource_pool, host):
+    def relocate_backing(
+            self, backing, datastore, resource_pool, host, disk_type=None):
         """Relocates backing to the input datastore and resource pool.
 
         The implementation uses moveAllDiskBackingsAndAllowSharing disk move
@@ -841,15 +842,27 @@ class VMwareVolumeOps(object):
         :param datastore: Reference to the datastore
         :param resource_pool: Reference to the resource pool
         :param host: Reference to the host
+        :param disk_type: destination disk type
         """
         LOG.debug("Relocating backing: %(backing)s to datastore: %(ds)s "
-                  "and resource pool: %(rp)s." %
-                  {'backing': backing, 'ds': datastore, 'rp': resource_pool})
+                  "and resource pool: %(rp)s with destination disk type: "
+                  "%(disk_type)s.",
+                  {'backing': backing,
+                   'ds': datastore,
+                   'rp': resource_pool,
+                   'disk_type': disk_type})
 
         # Relocate the volume backing
         disk_move_type = 'moveAllDiskBackingsAndAllowSharing'
+
+        disk_device = None
+        if disk_type is not None:
+            disk_device = self._get_disk_device(backing)
+
         relocate_spec = self._get_relocate_spec(datastore, resource_pool, host,
-                                                disk_move_type)
+                                                disk_move_type, disk_type,
+                                                disk_device)
+
         task = self._session.invoke_api(self._session.vim, 'RelocateVM_Task',
                                         backing, spec=relocate_spec)
         LOG.debug("Initiated relocation of volume backing: %s." % backing)
@@ -1044,6 +1057,19 @@ class VMwareVolumeOps(object):
         LOG.info(_("Successfully created clone: %s.") % new_backing)
         return new_backing
 
+    def _reconfigure_backing(self, backing, reconfig_spec):
+        """Reconfigure backing VM with the given spec."""
+        LOG.debug("Reconfiguring backing VM: %(backing)s with spec: %(spec)s.",
+                  {'backing': backing,
+                   'spec': reconfig_spec})
+        reconfig_task = self._session.invoke_api(self._session.vim,
+                                                 "ReconfigVM_Task",
+                                                 backing,
+                                                 spec=reconfig_spec)
+        LOG.debug("Task: %s created for reconfiguring backing VM.",
+                  reconfig_task)
+        self._session.wait_for_task(reconfig_task)
+
     def attach_disk_to_backing(self, backing, size_in_kb, disk_type,
                                adapter_type, vmdk_ds_file_path):
         """Attach an existing virtual disk to the backing VM.
@@ -1055,6 +1081,13 @@ class VMwareVolumeOps(object):
         :param vmdk_ds_file_path: datastore file path of the virtual disk to
                                   be attached
         """
+        LOG.debug("Reconfiguring backing VM: %(backing)s to add new disk: "
+                  "%(path)s with size (KB): %(size)d and adapter type: "
+                  "%(adapter_type)s.",
+                  {'backing': backing,
+                   'path': vmdk_ds_file_path,
+                   'size': size_in_kb,
+                   'adapter_type': adapter_type})
         cf = self._session.vim.client.factory
         reconfig_spec = cf.create('ns0:VirtualMachineConfigSpec')
         specs = self._create_specs_for_disk_add(size_in_kb,
@@ -1062,16 +1095,7 @@ class VMwareVolumeOps(object):
                                                 adapter_type,
                                                 vmdk_ds_file_path)
         reconfig_spec.deviceChange = specs
-        LOG.debug("Reconfiguring backing VM: %(backing)s with spec: %(spec)s.",
-                  {'backing': backing,
-                   'spec': reconfig_spec})
-        reconfig_task = self._session.invoke_api(self._session.vim,
-                                                 "ReconfigVM_Task",
-                                                 backing,
-                                                 spec=reconfig_spec)
-        LOG.debug("Task: %s created for reconfiguring backing VM.",
-                  reconfig_task)
-        self._session.wait_for_task(reconfig_task)
+        self._reconfigure_backing(backing, reconfig_spec)
         LOG.debug("Backing VM: %s reconfigured with new disk.", backing)
 
     def rename_backing(self, backing, new_name):
@@ -1093,6 +1117,32 @@ class VMwareVolumeOps(object):
                  {'backing': backing,
                   'new_name': new_name})
 
+    def change_backing_profile(self, backing, profile_id):
+        """Change storage profile of the backing VM.
+
+        The current profile is removed if the new profile is None.
+        """
+        LOG.debug("Reconfiguring backing VM: %(backing)s to change profile to:"
+                  " %(profile)s.",
+                  {'backing': backing,
+                   'profile': profile_id})
+        cf = self._session.vim.client.factory
+        reconfig_spec = cf.create('ns0:VirtualMachineConfigSpec')
+
+        if profile_id is None:
+            vm_profile = cf.create('ns0:VirtualMachineEmptyProfileSpec')
+            vm_profile.dynamicType = 'profile'
+        else:
+            vm_profile = cf.create('ns0:VirtualMachineDefinedProfileSpec')
+            vm_profile.profileId = profile_id.uniqueId
+
+        reconfig_spec.vmProfile = [vm_profile]
+        self._reconfigure_backing(backing, reconfig_spec)
+        LOG.debug("Backing VM: %(backing)s reconfigured with new profile: "
+                  "%(profile)s.",
+                  {'backing': backing,
+                   'profile': profile_id})
+
     def delete_file(self, file_path, datacenter=None):
         """Delete file or folder on the datastore.