From f36fc239804fb8fbf57d9df0320e2cb6d315ea10 Mon Sep 17 00:00:00 2001 From: Vipin Balachandran Date: Tue, 5 Sep 2017 15:00:28 -0700 Subject: [PATCH] VMware: Use vSphere template as snapshot format Adding support for using vSphere template as a volume snapshot format in vCenter server. The current format (COW disk) does not allow snapshot of attached volumes and this limitation is removed by the new template format. Change-Id: Id5dd71a785c4cd72ba44f9b4d26319be53079c39 --- .../volume/drivers/vmware/test_vmware_vmdk.py | 371 +++++++++++++++--- .../drivers/vmware/test_vmware_volumeops.py | 16 + cinder/volume/drivers/vmware/exceptions.py | 5 + cinder/volume/drivers/vmware/vmdk.py | 162 ++++++-- cinder/volume/drivers/vmware/volumeops.py | 8 + ...dk-snapshot-template-d3dcfc0906c02edd.yaml | 11 + 6 files changed, 497 insertions(+), 76 deletions(-) create mode 100644 releasenotes/notes/vmware-vmdk-snapshot-template-d3dcfc0906c02edd.yaml diff --git a/cinder/tests/unit/volume/drivers/vmware/test_vmware_vmdk.py b/cinder/tests/unit/volume/drivers/vmware/test_vmware_vmdk.py index 10351d57be6..8ee2354700c 100644 --- a/cinder/tests/unit/volume/drivers/vmware/test_vmware_vmdk.py +++ b/cinder/tests/unit/volume/drivers/vmware/test_vmware_vmdk.py @@ -61,6 +61,7 @@ class VMwareVcVmdkDriverTestCase(test.TestCase): CLUSTERS = ["cls-1", "cls-2"] DEFAULT_VC_VERSION = '5.5' POOL_SIZE = 20 + SNAPSHOT_FORMAT = 'COW' VOL_ID = 'abcdefab-cdef-abcd-efab-cdefabcdefab' SRC_VOL_ID = '9b3f6f1b-03a9-4f1e-aaff-ae15122b6ccf' @@ -96,6 +97,7 @@ class VMwareVcVmdkDriverTestCase(test.TestCase): self._config.vmware_host_version = self.DEFAULT_VC_VERSION self._config.vmware_connection_pool_size = self.POOL_SIZE self._config.vmware_adapter_type = self.ADAPTER_TYPE + self._config.vmware_snapshot_format = self.SNAPSHOT_FORMAT self._db = mock.Mock() self._driver = vmdk.VMwareVcVmdkDriver(configuration=self._config, @@ -254,45 +256,197 @@ class VMwareVcVmdkDriverTestCase(test.TestCase): volume, snap_id=SNAPSHOT_ID, name=SNAPSHOT_NAME, - description=SNAPSHOT_DESCRIPTION): + description=SNAPSHOT_DESCRIPTION, + provider_location=None): return {'id': snap_id, 'volume': volume, 'volume_name': volume['name'], 'name': name, 'display_description': description, - 'volume_size': volume['size'] + 'volume_size': volume['size'], + 'provider_location': provider_location } @mock.patch.object(VMDK_DRIVER, 'volumeops') - def test_create_snapshot_without_backing(self, vops): + @mock.patch.object(VMDK_DRIVER, '_get_volume_group_folder') + def test_get_snapshot_group_folder(self, get_volume_group_folder, vops): + dc = mock.sentinel.dc + vops.get_dc.return_value = dc + + folder = mock.sentinel.folder + get_volume_group_folder.return_value = folder + + volume = self._create_volume_obj() + backing = mock.sentinel.backing + self.assertEqual(folder, self._driver._get_snapshot_group_folder( + volume, backing)) + vops.get_dc.assert_called_once_with(backing) + get_volume_group_folder.assert_called_once_with( + dc, volume.project_id, snapshot=True) + + @mock.patch.object(VMDK_DRIVER, '_get_snapshot_group_folder') + @mock.patch.object(VMDK_DRIVER, 'volumeops') + @mock.patch.object(VMDK_DRIVER, '_in_use') + @mock.patch.object(VMDK_DRIVER, '_create_temp_backing_from_attached_vmdk') + @mock.patch.object(VMDK_DRIVER, '_delete_temp_backing') + def _test_create_snapshot_template_format( + self, delete_temp_backing, create_temp_backing_from_attached_vmdk, + in_use, vops, get_snapshot_group_folder, attached=False, + mark_as_template_error=False): + folder = mock.sentinel.folder + get_snapshot_group_folder.return_value = folder + + datastore = mock.sentinel.datastore + vops.get_datastore.return_value = datastore + + tmp_backing = mock.sentinel.tmp_backing + if attached: + in_use.return_value = True + create_temp_backing_from_attached_vmdk.return_value = tmp_backing + else: + in_use.return_value = False + vops.clone_backing.return_value = tmp_backing + + if mark_as_template_error: + vops.mark_backing_as_template.side_effect = ( + exceptions.VimException()) + else: + inv_path = mock.sentinel.inv_path + vops.get_inventory_path.return_value = inv_path + + volume = self._create_volume_obj() + snapshot = fake_snapshot.fake_snapshot_obj( + self._context, volume=volume) + backing = mock.sentinel.backing + if mark_as_template_error: + self.assertRaises( + exceptions.VimException, + self._driver._create_snapshot_template_format, + snapshot, + backing) + delete_temp_backing.assert_called_once_with(tmp_backing) + else: + exp_result = {'provider_location': inv_path} + self.assertEqual(exp_result, + self._driver._create_snapshot_template_format( + snapshot, backing)) + delete_temp_backing.assert_not_called() + get_snapshot_group_folder.test_assert_called_once_with(volume, backing) + vops.get_datastore.assert_called_once_with(backing) + in_use.assert_called_once_with(snapshot.volume) + if attached: + create_temp_backing_from_attached_vmdk.assert_called_once_with( + snapshot.volume, None, None, folder, datastore, + tmp_name=snapshot.name) + else: + vops.clone_backing.assert_called_once_with( + snapshot.name, backing, None, volumeops.FULL_CLONE_TYPE, + datastore, folder=folder) + vops.mark_backing_as_template.assert_called_once_with(tmp_backing) + + def test_create_snapshot_template_format(self): + self._test_create_snapshot_template_format() + + def test_create_snapshot_template_format_force(self): + self._test_create_snapshot_template_format(attached=True) + + def test_create_snapshot_template_format_mark_template_error(self): + self._test_create_snapshot_template_format(mark_as_template_error=True) + + @mock.patch.object(VMDK_DRIVER, '_in_use', return_value=False) + @mock.patch.object(VMDK_DRIVER, 'volumeops') + def test_create_snapshot_without_backing(self, vops, in_use): vops.get_backing.return_value = None volume = self._create_volume_dict() snapshot = self._create_snapshot_dict(volume) - self._driver.create_snapshot(snapshot) + ret = self._driver.create_snapshot(snapshot) + self.assertIsNone(ret) vops.get_backing.assert_called_once_with(snapshot['volume_name']) self.assertFalse(vops.create_snapshot.called) + @mock.patch.object(VMDK_DRIVER, '_in_use', return_value=False) @mock.patch.object(VMDK_DRIVER, 'volumeops') - def test_create_snapshot_with_backing(self, vops): + def test_create_snapshot_with_backing(self, vops, in_use): backing = mock.sentinel.backing vops.get_backing.return_value = backing volume = self._create_volume_dict() snapshot = self._create_snapshot_dict(volume) - self._driver.create_snapshot(snapshot) + ret = self._driver.create_snapshot(snapshot) + self.assertIsNone(ret) vops.get_backing.assert_called_once_with(snapshot['volume_name']) vops.create_snapshot.assert_called_once_with( backing, snapshot['name'], snapshot['display_description']) - def test_create_snapshot_when_attached(self): + @mock.patch.object(VMDK_DRIVER, '_in_use', return_value=True) + def test_create_snapshot_when_attached(self, in_use): volume = self._create_volume_dict(status='in-use') snapshot = self._create_snapshot_dict(volume) self.assertRaises(cinder_exceptions.InvalidVolume, self._driver.create_snapshot, snapshot) + @mock.patch.object(VMDK_DRIVER, '_in_use', return_value=True) + @mock.patch.object(VMDK_DRIVER, 'volumeops') + @mock.patch.object(VMDK_DRIVER, '_create_snapshot_template_format') + def test_create_snapshot_template( + self, create_snapshot_template_format, vops, in_use): + self._driver.configuration.vmware_snapshot_format = 'template' + + backing = mock.sentinel.backing + vops.get_backing.return_value = backing + + model_update = mock.sentinel.model_update + create_snapshot_template_format.return_value = model_update + + volume = self._create_volume_dict() + snapshot = self._create_snapshot_dict(volume) + ret = self._driver.create_snapshot(snapshot) + + self.assertEqual(model_update, ret) + vops.get_backing.assert_called_once_with(snapshot['volume_name']) + create_snapshot_template_format.assert_called_once_with( + snapshot, backing) + + @mock.patch.object(VMDK_DRIVER, 'volumeops') + def test_get_template_by_inv_path(self, vops): + template = mock.sentinel.template + vops.get_entity_by_inventory_path.return_value = template + + inv_path = mock.sentinel.inv_path + self.assertEqual(template, + self._driver._get_template_by_inv_path(inv_path)) + vops.get_entity_by_inventory_path.assert_called_once_with(inv_path) + + @mock.patch.object(VMDK_DRIVER, 'volumeops') + def test_get_template_by_inv_path_invalid_path(self, vops): + vops.get_entity_by_inventory_path.return_value = None + + inv_path = mock.sentinel.inv_path + self.assertRaises(vmdk_exceptions.TemplateNotFoundException, + self._driver._get_template_by_inv_path, + inv_path) + vops.get_entity_by_inventory_path.assert_called_once_with(inv_path) + + @mock.patch.object(VMDK_DRIVER, '_get_template_by_inv_path') + @mock.patch.object(VMDK_DRIVER, 'volumeops') + def test_delete_snapshot_template_format( + self, vops, get_template_by_inv_path): + template = mock.sentinel.template + get_template_by_inv_path.return_value = template + + inv_path = '/dc-1/vm/foo' + volume = self._create_volume_dict() + snapshot = fake_snapshot.fake_snapshot_obj(self._context, + volume=volume, + provider_location=inv_path) + self._driver._delete_snapshot_template_format(snapshot) + + get_template_by_inv_path.assert_called_once_with(inv_path) + vops.delete_backing.assert_called_once_with(template) + @mock.patch.object(VMDK_DRIVER, 'volumeops') def test_delete_snapshot_without_backing(self, vops): vops.get_backing.return_value = None @@ -350,6 +504,26 @@ class VMwareVcVmdkDriverTestCase(test.TestCase): vops.get_snapshot.assert_called_once_with(backing, snapshot.name) vops.delete_snapshot.assert_not_called() + @mock.patch.object(VMDK_DRIVER, 'volumeops') + @mock.patch.object(VMDK_DRIVER, '_in_use', return_value=True) + @mock.patch.object(VMDK_DRIVER, '_delete_snapshot_template_format') + def test_delete_snapshot_template( + self, delete_snapshot_template_format, in_use, vops): + backing = mock.sentinel.backing + vops.get_backing.return_value = backing + + inv_path = '/dc-1/vm/foo' + volume = self._create_volume_dict(status='deleting') + snapshot = fake_snapshot.fake_snapshot_obj(self._context, + volume=volume, + provider_location=inv_path) + self._driver.delete_snapshot(snapshot) + + vops.get_backing.assert_called_once_with(snapshot.volume_name) + vops.get_snapshot.assert_not_called() + in_use.assert_called_once_with(snapshot.volume) + delete_snapshot_template_format.assert_called_once_with(snapshot) + @ddt.data('vmdk', 'VMDK', None) def test_validate_disk_format(self, disk_format): self._driver._validate_disk_format(disk_format) @@ -1605,18 +1779,29 @@ class VMwareVcVmdkDriverTestCase(test.TestCase): self._test_initialize_connection(instance_exists=False) @mock.patch.object(VMDK_DRIVER, 'volumeops') - def test_get_volume_group_folder(self, vops): + def _test_get_volume_group_folder(self, vops, snapshot=False): folder = mock.sentinel.folder vops.create_vm_inventory_folder.return_value = folder datacenter = mock.sentinel.dc project_id = '63c19a12292549818c09946a5e59ddaf' self.assertEqual(folder, - self._driver._get_volume_group_folder(datacenter, - project_id)) + self._driver._get_volume_group_folder( + datacenter, project_id, snapshot=snapshot)) project_folder_name = 'Project (%s)' % project_id + exp_folder_names = ['OpenStack', + project_folder_name, + self.VOLUME_FOLDER] + if snapshot: + exp_folder_names.append('Snapshots') vops.create_vm_inventory_folder.assert_called_once_with( - datacenter, ['OpenStack', project_folder_name, self.VOLUME_FOLDER]) + datacenter, exp_folder_names) + + def test_get_volume_group_folder(self): + self._test_get_volume_group_folder() + + def test_get_volume_group_folder_for_snapshot(self): + self._test_get_volume_group_folder(snapshot=True) @mock.patch('cinder.volume.drivers.vmware.vmdk.' '_get_volume_type_extra_spec') @@ -1725,6 +1910,53 @@ class VMwareVcVmdkDriverTestCase(test.TestCase): self._test_clone_backing( clone_type=volumeops.LINKED_CLONE_TYPE, vc60=True) + @mock.patch.object(VMDK_DRIVER, '_get_template_by_inv_path') + @mock.patch('oslo_utils.uuidutils.generate_uuid') + @mock.patch.object(VMDK_DRIVER, '_select_ds_for_volume') + @mock.patch.object(VMDK_DRIVER, '_get_disk_type') + @mock.patch.object(VMDK_DRIVER, 'volumeops') + @mock.patch.object(VMDK_DRIVER, '_create_volume_from_temp_backing') + def test_create_volume_from_template( + self, create_volume_from_temp_backing, vops, get_disk_type, + select_ds_for_volume, generate_uuid, get_template_by_inv_path): + template = mock.sentinel.template + get_template_by_inv_path.return_value = template + + tmp_name = 'de4c648c-8403-4dcc-b14a-d2541b7cba2b' + generate_uuid.return_value = tmp_name + + host = mock.sentinel.host + rp = mock.sentinel.rp + folder = mock.sentinel.folder + datastore = mock.sentinel.datastore + summary = mock.Mock(datastore=datastore) + select_ds_for_volume.return_value = (host, rp, folder, summary) + + disk_type = mock.sentinel.disk_type + get_disk_type.return_value = disk_type + + tmp_backing = mock.sentinel.tmp_backing + vops.clone_backing.return_value = tmp_backing + + volume = self._create_volume_obj() + inv_path = mock.sentinel.inv_path + self._driver._create_volume_from_template(volume, inv_path) + + get_template_by_inv_path.assert_called_once_with(inv_path) + select_ds_for_volume.assert_called_once_with(volume) + get_disk_type.assert_called_once_with(volume) + vops.clone_backing.assert_called_once_with(tmp_name, + template, + None, + volumeops.FULL_CLONE_TYPE, + datastore, + disk_type=disk_type, + host=host, + resource_pool=rp, + folder=folder) + create_volume_from_temp_backing.assert_called_once_with(volume, + tmp_backing) + @mock.patch.object(VMDK_DRIVER, 'volumeops') @mock.patch.object(VMDK_DRIVER, '_clone_backing') def test_create_volume_from_snapshot_without_backing(self, clone_backing, @@ -1759,9 +1991,11 @@ class VMwareVcVmdkDriverTestCase(test.TestCase): @mock.patch.object(VMDK_DRIVER, 'volumeops') @mock.patch.object(VMDK_DRIVER, '_get_clone_type') + @mock.patch.object(VMDK_DRIVER, '_create_volume_from_template') @mock.patch.object(VMDK_DRIVER, '_clone_backing') - def test_create_volume_from_snapshot(self, clone_backing, get_clone_type, - vops): + def _test_create_volume_from_snapshot( + self, clone_backing, create_volume_from_template, get_clone_type, + vops, template=False): backing = mock.sentinel.backing vops.get_backing.return_value = backing @@ -1772,15 +2006,31 @@ class VMwareVcVmdkDriverTestCase(test.TestCase): volume = self._create_volume_dict() src_vref = self._create_volume_dict(vol_id=self.SRC_VOL_ID) - snapshot = self._create_snapshot_dict(src_vref) + if template: + provider_location = mock.sentinel.inv_path + else: + provider_location = None + snapshot = self._create_snapshot_dict( + src_vref, provider_location=provider_location) self._driver.create_volume_from_snapshot(volume, snapshot) vops.get_backing.assert_called_once_with(snapshot['volume_name']) - vops.get_snapshot.assert_called_once_with(backing, snapshot['name']) - get_clone_type.assert_called_once_with(volume) - clone_backing.assert_called_once_with( - volume, backing, snapshot_moref, volumeops.FULL_CLONE_TYPE, - snapshot['volume_size']) + if template: + create_volume_from_template.assert_called_once_with( + volume, mock.sentinel.inv_path) + else: + vops.get_snapshot.assert_called_once_with(backing, + snapshot['name']) + get_clone_type.assert_called_once_with(volume) + clone_backing.assert_called_once_with( + volume, backing, snapshot_moref, volumeops.FULL_CLONE_TYPE, + snapshot['volume_size']) + + def test_create_volume_from_snapshot(self): + self._test_create_volume_from_snapshot() + + def test_create_volume_from_snapshot_template(self): + self._test_create_volume_from_snapshot(template=True) @mock.patch.object(VMDK_DRIVER, 'session') def test_get_volume_device_uuid(self, session): @@ -1799,12 +2049,8 @@ class VMwareVcVmdkDriverTestCase(test.TestCase): @mock.patch.object(VMDK_DRIVER, 'volumeops') @mock.patch.object(VMDK_DRIVER, '_get_volume_device_uuid') @mock.patch('oslo_utils.uuidutils.generate_uuid') - @mock.patch.object(VMDK_DRIVER, '_select_ds_for_volume') - @mock.patch.object(VMDK_DRIVER, '_manage_existing_int') - @mock.patch.object(VMDK_DRIVER, '_delete_temp_backing') - def test_clone_attached_volume( - self, delete_temp_backing, manage_existing_int, - select_ds_for_volume, generate_uuid, get_volume_device_uuid, vops): + def test_create_temp_backing_from_attached_vmdk( + self, generate_uuid, get_volume_device_uuid, vops): instance = mock.sentinel.instance vops.get_backing_by_uuid.return_value = instance @@ -1814,6 +2060,53 @@ class VMwareVcVmdkDriverTestCase(test.TestCase): tmp_name = mock.sentinel.tmp_name generate_uuid.return_value = tmp_name + tmp_backing = mock.sentinel.tmp_backing + vops.clone_backing.return_value = tmp_backing + + instance_uuid = fake_constants.INSTANCE_ID + attachment = fake_volume.fake_db_volume_attachment( + instance_uuid=instance_uuid) + src_vref = self._create_volume_dict(vol_id=fake_constants.VOLUME_ID, + attachment=[attachment]) + host = mock.sentinel.host + rp = mock.sentinel.rp + folder = mock.sentinel.folder + datastore = mock.sentinel.datastore + ret = self._driver._create_temp_backing_from_attached_vmdk( + src_vref, host, rp, folder, datastore) + + self.assertEqual(tmp_backing, ret) + vops.get_backing_by_uuid.assert_called_once_with(instance_uuid) + get_volume_device_uuid.assert_called_once_with(instance, + src_vref['id']) + vops.clone_backing.assert_called_once_with( + tmp_name, instance, None, volumeops.FULL_CLONE_TYPE, datastore, + host=host, resource_pool=rp, folder=folder, + disks_to_clone=[vol_dev_uuid]) + + @mock.patch.object(VMDK_DRIVER, 'volumeops') + @mock.patch.object(VMDK_DRIVER, '_manage_existing_int') + @mock.patch.object(VMDK_DRIVER, '_delete_temp_backing') + def test_create_volume_from_temp_backing( + self, delete_temp_backing, manage_existing_int, vops): + disk_device = mock.sentinel.disk_device + vops._get_disk_device.return_value = disk_device + + volume = self._create_volume_dict() + tmp_backing = mock.sentinel.tmp_backing + self._driver._create_volume_from_temp_backing(volume, tmp_backing) + + vops._get_disk_device.assert_called_once_with(tmp_backing) + manage_existing_int.assert_called_once_with( + volume, tmp_backing, disk_device) + delete_temp_backing.assert_called_once_with(tmp_backing) + + @mock.patch.object(VMDK_DRIVER, '_select_ds_for_volume') + @mock.patch.object(VMDK_DRIVER, '_create_temp_backing_from_attached_vmdk') + @mock.patch.object(VMDK_DRIVER, '_create_volume_from_temp_backing') + def test_clone_attached_volume( + self, create_volume_from_temp_backing, + create_temp_backing_from_attached_vmdk, select_ds_for_volume): host = mock.sentinel.host rp = mock.sentinel.rp folder = mock.sentinel.folder @@ -1822,31 +2115,17 @@ class VMwareVcVmdkDriverTestCase(test.TestCase): select_ds_for_volume.return_value = (host, rp, folder, summary) tmp_backing = mock.sentinel.tmp_backing - vops.clone_backing.return_value = tmp_backing + create_temp_backing_from_attached_vmdk.return_value = tmp_backing - disk_device = mock.sentinel.disk_device - vops._get_disk_device.return_value = disk_device - - instance_uuid = fake_constants.INSTANCE_ID - attachment = fake_volume.fake_db_volume_attachment( - instance_uuid=instance_uuid) - src_vref = self._create_volume_dict(vol_id=fake_constants.VOLUME_ID, - attachment=[attachment]) - volume = self._create_volume_dict() + src_vref = mock.sentinel.src_vref + volume = mock.sentinel.volume self._driver._clone_attached_volume(src_vref, volume) - vops.get_backing_by_uuid.assert_called_once_with(instance_uuid) - get_volume_device_uuid.assert_called_once_with(instance, - src_vref['id']) select_ds_for_volume.assert_called_once_with(volume) - vops.clone_backing.assert_called_once_with( - tmp_name, instance, None, volumeops.FULL_CLONE_TYPE, datastore, - host=host, resource_pool=rp, folder=folder, - disks_to_clone=[vol_dev_uuid]) - vops._get_disk_device.assert_called_once_with(tmp_backing) - manage_existing_int.assert_called_once_with( - volume, tmp_backing, disk_device) - delete_temp_backing.assert_called_once_with(tmp_backing) + create_temp_backing_from_attached_vmdk.assert_called_once_with( + src_vref, host, rp, folder, datastore) + create_volume_from_temp_backing.assert_called_once_with( + volume, tmp_backing) @mock.patch.object(VMDK_DRIVER, 'volumeops') @mock.patch.object(VMDK_DRIVER, '_clone_backing') diff --git a/cinder/tests/unit/volume/drivers/vmware/test_vmware_volumeops.py b/cinder/tests/unit/volume/drivers/vmware/test_vmware_volumeops.py index 59fcf8bd01a..f437de92d08 100644 --- a/cinder/tests/unit/volume/drivers/vmware/test_vmware_volumeops.py +++ b/cinder/tests/unit/volume/drivers/vmware/test_vmware_volumeops.py @@ -1689,6 +1689,16 @@ class VolumeOpsTestCase(test.TestCase): self.session.vim.service_content.searchIndex, inventoryPath=path) + def test_get_inventory_path(self): + + path = mock.sentinel.path + self.session.invoke_api.return_value = path + + entity = mock.sentinel.entity + self.assertEqual(path, self.vops.get_inventory_path(entity)) + self.session.invoke_api.assert_called_once_with( + vim_util, 'get_inventory_path', self.session.vim, entity) + def test_get_disk_devices(self): disk_device = mock.Mock() disk_device.__class__.__name__ = 'VirtualDisk' @@ -1713,6 +1723,12 @@ class VolumeOpsTestCase(test.TestCase): backing.uuid = uuid return mock.Mock(backing=backing) + def test_mark_backing_as_template(self): + backing = mock.Mock() + self.vops.mark_backing_as_template(backing) + self.session.invoke_api.assert_called_once_with( + self.session.vim, 'MarkAsTemplate', backing) + @mock.patch('cinder.volume.drivers.vmware.volumeops.VMwareVolumeOps.' '_get_disk_devices') def test_get_disk_device(self, get_disk_devices): diff --git a/cinder/volume/drivers/vmware/exceptions.py b/cinder/volume/drivers/vmware/exceptions.py index 5917b1df01a..13b08a7e7f1 100644 --- a/cinder/volume/drivers/vmware/exceptions.py +++ b/cinder/volume/drivers/vmware/exceptions.py @@ -55,3 +55,8 @@ class ClusterNotFoundException(exceptions.VMwareDriverException): class NoValidHostException(exceptions.VMwareDriverException): """Thrown when there are no valid ESX hosts.""" msg_fmt = _("There are no valid ESX hosts.") + + +class TemplateNotFoundException(exceptions.VMwareDriverException): + """Thrown when template cannot be found.""" + msg_fmt = _("Template cannot be found at path: %(path)s.") diff --git a/cinder/volume/drivers/vmware/vmdk.py b/cinder/volume/drivers/vmware/vmdk.py index 4b67413fe1d..a5bf1acd9bf 100644 --- a/cinder/volume/drivers/vmware/vmdk.py +++ b/cinder/volume/drivers/vmware/vmdk.py @@ -137,6 +137,10 @@ vmdk_opts = [ volumeops.VirtualDiskAdapterType.IDE], default=volumeops.VirtualDiskAdapterType.LSI_LOGIC, help='Default adapter type to be used for attaching volumes.'), + cfg.StrOpt('vmware_snapshot_format', + choices=['template', 'COW'], + default='template', + help='Volume snapshot format in vCenter server.'), ] CONF = cfg.CONF @@ -658,6 +662,34 @@ class VMwareVcVmdkDriver(driver.VolumeDriver): def remove_export(self, context, volume): pass + def _get_snapshot_group_folder(self, volume, backing): + dc = self.volumeops.get_dc(backing) + return self._get_volume_group_folder( + dc, volume.project_id, snapshot=True) + + def _create_snapshot_template_format(self, snapshot, backing): + volume = snapshot.volume + folder = self._get_snapshot_group_folder(volume, backing) + datastore = self.volumeops.get_datastore(backing) + + if self._in_use(volume): + tmp_backing = self._create_temp_backing_from_attached_vmdk( + volume, None, None, folder, datastore, tmp_name=snapshot.name) + else: + tmp_backing = self.volumeops.clone_backing( + snapshot.name, backing, None, volumeops.FULL_CLONE_TYPE, + datastore, folder=folder) + + try: + self.volumeops.mark_backing_as_template(tmp_backing) + except exceptions.VimException: + with excutils.save_and_reraise_exception(): + LOG.error("Error marking temporary backing as template.") + self._delete_temp_backing(tmp_backing) + + return {'provider_location': + self.volumeops.get_inventory_path(tmp_backing)} + def _create_snapshot(self, snapshot): """Creates a snapshot. @@ -669,49 +701,78 @@ class VMwareVcVmdkDriver(driver.VolumeDriver): """ volume = snapshot['volume'] - if volume['status'] != 'available': + snapshot_format = self.configuration.vmware_snapshot_format + if self._in_use(volume) and snapshot_format == 'COW': msg = _("Snapshot of volume not supported in " "state: %s.") % volume['status'] LOG.error(msg) raise exception.InvalidVolume(msg) + backing = self.volumeops.get_backing(snapshot['volume_name']) if not backing: LOG.info("There is no backing, so will not create " "snapshot: %s.", snapshot['name']) return - self.volumeops.create_snapshot(backing, snapshot['name'], - snapshot['display_description']) + + model_update = None + if snapshot_format == 'COW': + self.volumeops.create_snapshot(backing, snapshot['name'], + snapshot['display_description']) + else: + model_update = self._create_snapshot_template_format( + snapshot, backing) + LOG.info("Successfully created snapshot: %s.", snapshot['name']) + return model_update def create_snapshot(self, snapshot): """Creates a snapshot. :param snapshot: Snapshot object """ - self._create_snapshot(snapshot) + return self._create_snapshot(snapshot) + + def _get_template_by_inv_path(self, inv_path): + template = self.volumeops.get_entity_by_inventory_path(inv_path) + if template is None: + LOG.error("Template not found at path: %s.", inv_path) + raise vmdk_exceptions.TemplateNotFoundException(path=inv_path) + else: + return template + + def _delete_snapshot_template_format(self, snapshot): + template = self._get_template_by_inv_path(snapshot.provider_location) + self.volumeops.delete_backing(template) def _delete_snapshot(self, snapshot): """Delete snapshot. If the volume does not have a backing or the snapshot does not exist - then simply pass, else delete the snapshot. Snapshot deletion of only - available volume is supported. + then simply pass, else delete the snapshot. The volume must not be + attached for deletion of snapshot in COW format. :param snapshot: Snapshot object """ + inv_path = snapshot.provider_location + is_template = inv_path is not None + backing = self.volumeops.get_backing(snapshot.volume_name) if not backing: LOG.debug("Backing does not exist for volume.", resource=snapshot.volume) - elif not self.volumeops.get_snapshot(backing, snapshot.name): + elif (not is_template and + not self.volumeops.get_snapshot(backing, snapshot.name)): LOG.debug("Snapshot does not exist in backend.", resource=snapshot) - elif self._in_use(snapshot.volume): + elif self._in_use(snapshot.volume) and not is_template: msg = _("Delete snapshot of volume not supported in " "state: %s.") % snapshot.volume.status LOG.error(msg) raise exception.InvalidSnapshot(reason=msg) else: - self.volumeops.delete_snapshot(backing, snapshot.name) + if is_template: + self._delete_snapshot_template_format(snapshot) + else: + self.volumeops.delete_snapshot(backing, snapshot.name) def delete_snapshot(self, snapshot): """Delete snapshot. @@ -1719,8 +1780,8 @@ class VMwareVcVmdkDriver(driver.VolumeDriver): "%(ip)s.", {'driver': self.__class__.__name__, 'ip': self.configuration.vmware_host_ip}) - def _get_volume_group_folder(self, datacenter, project_id): - """Get inventory folder for organizing volume backings. + def _get_volume_group_folder(self, datacenter, project_id, snapshot=False): + """Get inventory folder for organizing volume backings and snapshots. The inventory folder for organizing volume backings has the following hierarchy: @@ -1729,13 +1790,19 @@ class VMwareVcVmdkDriver(driver.VolumeDriver): where volume_folder is the vmdk driver config option "vmware_volume_folder". + A sub-folder named 'Snapshots' under volume_folder is used for + organizing snapshots in template format. + :param datacenter: Reference to the datacenter :param project_id: OpenStack project ID + :param snapshot: Return folder for snapshot if True :return: Reference to the inventory folder """ volume_folder_name = self.configuration.vmware_volume_folder project_folder_name = "Project (%s)" % project_id folder_names = ['OpenStack', project_folder_name, volume_folder_name] + if snapshot: + folder_names.append('Snapshots') return self.volumeops.create_vm_inventory_folder(datacenter, folder_names) @@ -1865,6 +1932,30 @@ class VMwareVcVmdkDriver(driver.VolumeDriver): self._extend_backing(clone, volume['size']) LOG.info("Successfully created clone: %s.", clone) + def _create_volume_from_template(self, volume, path): + LOG.debug("Creating backing for volume: %(volume_id)s from template " + "at path: %(path)s.", + {'volume_id': volume.id, + 'path': path}) + template = self._get_template_by_inv_path(path) + + # Create temporary backing by cloning the template. + tmp_name = uuidutils.generate_uuid() + (host, rp, folder, summary) = self._select_ds_for_volume(volume) + datastore = summary.datastore + disk_type = VMwareVcVmdkDriver._get_disk_type(volume) + tmp_backing = self.volumeops.clone_backing(tmp_name, + template, + None, + volumeops.FULL_CLONE_TYPE, + datastore, + disk_type=disk_type, + host=host, + resource_pool=rp, + folder=folder) + + self._create_volume_from_temp_backing(volume, tmp_backing) + def _create_volume_from_snapshot(self, volume, snapshot): """Creates a volume from a snapshot. @@ -1881,17 +1972,22 @@ class VMwareVcVmdkDriver(driver.VolumeDriver): "volume: %(vol)s.", {'snap': snapshot['name'], 'vol': volume['name']}) return - snapshot_moref = self.volumeops.get_snapshot(backing, - snapshot['name']) - if not snapshot_moref: - LOG.info("There is no snapshot point for the snapshotted " - "volume: %(snap)s. Not creating any backing for " - "the volume: %(vol)s.", - {'snap': snapshot['name'], 'vol': volume['name']}) - return - clone_type = VMwareVcVmdkDriver._get_clone_type(volume) - self._clone_backing(volume, backing, snapshot_moref, clone_type, - snapshot['volume_size']) + + inv_path = snapshot.get('provider_location') + if inv_path: + self._create_volume_from_template(volume, inv_path) + else: + snapshot_moref = self.volumeops.get_snapshot(backing, + snapshot['name']) + if not snapshot_moref: + LOG.info("There is no snapshot point for the snapshotted " + "volume: %(snap)s. Not creating any backing for " + "the volume: %(vol)s.", + {'snap': snapshot['name'], 'vol': volume['name']}) + return + clone_type = VMwareVcVmdkDriver._get_clone_type(volume) + self._clone_backing(volume, backing, snapshot_moref, clone_type, + snapshot['volume_size']) def create_volume_from_snapshot(self, volume, snapshot): """Creates a volume from a snapshot. @@ -1911,7 +2007,8 @@ class VMwareVcVmdkDriver(driver.VolumeDriver): if opt_val is not None: return opt_val.value - def _clone_attached_volume(self, src_vref, volume): + def _create_temp_backing_from_attached_vmdk( + self, src_vref, host, rp, folder, datastore, tmp_name=None): instance = self.volumeops.get_backing_by_uuid( src_vref['volume_attachment'][0]['instance_uuid']) vol_dev_uuid = self._get_volume_device_uuid(instance, src_vref['id']) @@ -1919,23 +2016,28 @@ class VMwareVcVmdkDriver(driver.VolumeDriver): "%(instance)s.", {'dev': vol_dev_uuid, 'instance': instance}) - # Clone the vmdk attached to the instance to create a temporary - # backing. - tmp_name = uuidutils.generate_uuid() - (host, rp, folder, summary) = self._select_ds_for_volume(volume) - datastore = summary.datastore - tmp_backing = self.volumeops.clone_backing( + tmp_name = tmp_name or uuidutils.generate_uuid() + return self.volumeops.clone_backing( tmp_name, instance, None, volumeops.FULL_CLONE_TYPE, datastore, host=host, resource_pool=rp, folder=folder, disks_to_clone=[vol_dev_uuid]) + def _create_volume_from_temp_backing(self, volume, tmp_backing): try: - # Create volume from temporary backing. disk_device = self.volumeops._get_disk_device(tmp_backing) self._manage_existing_int(volume, tmp_backing, disk_device) finally: self._delete_temp_backing(tmp_backing) + def _clone_attached_volume(self, src_vref, volume): + # Clone the vmdk attached to the instance to create a temporary + # backing. + (host, rp, folder, summary) = self._select_ds_for_volume(volume) + datastore = summary.datastore + tmp_backing = self._create_temp_backing_from_attached_vmdk( + src_vref, host, rp, folder, datastore) + self._create_volume_from_temp_backing(volume, tmp_backing) + def _create_cloned_volume(self, volume, src_vref): """Creates volume clone. diff --git a/cinder/volume/drivers/vmware/volumeops.py b/cinder/volume/drivers/vmware/volumeops.py index 46642dd7d6b..643a5da6193 100644 --- a/cinder/volume/drivers/vmware/volumeops.py +++ b/cinder/volume/drivers/vmware/volumeops.py @@ -1618,6 +1618,10 @@ class VMwareVolumeOps(object): self._session.vim.service_content.searchIndex, inventoryPath=path) + def get_inventory_path(self, entity): + return self._session.invoke_api( + vim_util, 'get_inventory_path', self._session.vim, entity) + def _get_disk_devices(self, vm): disk_devices = [] hardware_devices = self._session.invoke_api(vim_util, @@ -1649,3 +1653,7 @@ class VMwareVolumeOps(object): if (backing.__class__.__name__ == "VirtualDiskFlatVer2BackingInfo" and backing.fileName == vmdk_path): return disk_device + + def mark_backing_as_template(self, backing): + LOG.debug("Marking backing: %s as template.", backing) + self._session.invoke_api(self._session.vim, 'MarkAsTemplate', backing) diff --git a/releasenotes/notes/vmware-vmdk-snapshot-template-d3dcfc0906c02edd.yaml b/releasenotes/notes/vmware-vmdk-snapshot-template-d3dcfc0906c02edd.yaml new file mode 100644 index 00000000000..baf8e8eb2d7 --- /dev/null +++ b/releasenotes/notes/vmware-vmdk-snapshot-template-d3dcfc0906c02edd.yaml @@ -0,0 +1,11 @@ +--- +features: + - | + VMware VMDK driver now supports vSphere template as a + volume snapshot format in vCenter server. The snapshot + format in vCenter server can be specified using driver + config option ``vmware_snapshot_format``. +upgrade: + - | + VMware VMDK driver will use vSphere template as the + default snapshot format in vCenter server.