diff --git a/cinder/tests/unit/volume/drivers/test_infinidat.py b/cinder/tests/unit/volume/drivers/test_infinidat.py index 54ed6fb5359..f8acaf7c85c 100644 --- a/cinder/tests/unit/volume/drivers/test_infinidat.py +++ b/cinder/tests/unit/volume/drivers/test_infinidat.py @@ -27,8 +27,10 @@ TEST_WWN_1 = '00:11:22:33:44:55:66:77' TEST_WWN_2 = '11:11:22:33:44:55:66:77' test_volume = mock.Mock(id=1, size=1) -test_snapshot = mock.Mock(id=2, volume=test_volume) +test_snapshot = mock.Mock(id=2, volume=test_volume, volume_id='1') test_clone = mock.Mock(id=3, size=1) +test_group = mock.Mock(id=4) +test_snapgroup = mock.Mock(id=5, group=test_group) test_connector = dict(wwpns=[TEST_WWN_1], initiator='iqn.2012-07.org.fake:01') @@ -86,10 +88,13 @@ class InfiniboxDriverTestCaseBase(test.TestCase): self._mock_pool.get_physical_capacity.return_value = units.Gi self._mock_ns = mock.Mock() self._mock_ns.get_ips.return_value = [mock.Mock(ip_address='1.1.1.1')] + self._mock_group = mock.Mock() result.volumes.safe_get.return_value = self._mock_volume result.volumes.create.return_value = self._mock_volume result.pools.safe_get.return_value = self._mock_pool result.hosts.safe_get.return_value = self._mock_host + result.cons_groups.safe_get.return_value = self._mock_group + result.cons_groups.create.return_value = self._mock_group result.hosts.create.return_value = self._mock_host result.network_spaces.safe_get.return_value = self._mock_ns result.components.nodes.get_all.return_value = [] @@ -302,6 +307,132 @@ class InfiniboxDriverTestCase(InfiniboxDriverTestCaseBase): self.driver.create_cloned_volume, test_clone, test_volume) + @mock.patch('cinder.volume.utils.is_group_a_cg_snapshot_type', + return_value=True) + def test_create_group(self, *mocks): + self.driver.create_group(None, test_group) + + @mock.patch('cinder.volume.utils.is_group_a_cg_snapshot_type', + return_value=True) + def test_create_group_twice(self, *mocks): + self.driver.create_group(None, test_group) + self.driver.create_group(None, test_group) + + @mock.patch('cinder.volume.utils.is_group_a_cg_snapshot_type', + return_value=True) + def test_create_group_api_fail(self, *mocks): + self._system.cons_groups.create.side_effect = self._raise_infinisdk + self.assertRaises(exception.VolumeBackendAPIException, + self.driver.create_group, + None, test_group) + + @mock.patch('cinder.volume.utils.is_group_a_cg_snapshot_type', + return_value=True) + def test_delete_group(self, *mocks): + self.driver.delete_group(None, test_group, [test_volume]) + + @mock.patch('cinder.volume.utils.is_group_a_cg_snapshot_type', + return_value=True) + def test_delete_group_doesnt_exist(self, *mocks): + self._system.cons_groups.safe_get.return_value = None + self.driver.delete_group(None, test_group, [test_volume]) + + @mock.patch('cinder.volume.utils.is_group_a_cg_snapshot_type', + return_value=True) + def test_delete_group_api_fail(self, *mocks): + self._mock_group.safe_delete.side_effect = self._raise_infinisdk + self.assertRaises(exception.VolumeBackendAPIException, + self.driver.delete_group, + None, test_group, [test_volume]) + + @mock.patch('cinder.volume.utils.is_group_a_cg_snapshot_type', + return_value=True) + def test_update_group_add_and_remove(self, *mocks): + self.driver.update_group(None, test_group, + [test_volume], [test_volume]) + + @mock.patch('cinder.volume.utils.is_group_a_cg_snapshot_type', + return_value=True) + def test_update_group_api_fail(self, *mocks): + self._mock_group.add_member.side_effect = self._raise_infinisdk + self.assertRaises(exception.VolumeBackendAPIException, + self.driver.update_group, + None, test_group, + [test_volume], [test_volume]) + + @mock.patch("cinder.volume.utils.copy_volume") + @mock.patch("cinder.utils.brick_get_connector") + @mock.patch("cinder.utils.brick_get_connector_properties", + return_value=test_connector) + @mock.patch('cinder.volume.utils.is_group_a_cg_snapshot_type', + return_value=True) + def test_create_group_from_src_snaps(self, *mocks): + self.driver.create_group_from_src(None, test_group, [test_volume], + test_snapgroup, [test_snapshot], + None, None) + + @mock.patch("cinder.volume.utils.copy_volume") + @mock.patch("cinder.utils.brick_get_connector") + @mock.patch("cinder.utils.brick_get_connector_properties", + return_value=test_connector) + @mock.patch('cinder.volume.utils.is_group_a_cg_snapshot_type', + return_value=True) + def test_create_group_from_src_vols(self, *mocks): + self.driver.create_group_from_src(None, test_group, [test_volume], + None, None, + test_group, [test_volume]) + + @mock.patch('cinder.volume.utils.is_group_a_cg_snapshot_type', + return_value=True) + def test_create_group_snap(self, *mocks): + mock_snapgroup = mock.Mock() + mock_snapgroup.get_members.return_value = [self._mock_volume] + self._mock_volume.get_parent.return_value = self._mock_volume + self._mock_volume.get_name.return_value = '' + self._mock_group.create_snapshot.return_value = mock_snapgroup + self.driver.create_group_snapshot(None, + test_snapgroup, + [test_snapshot]) + + @mock.patch('cinder.volume.utils.is_group_a_cg_snapshot_type', + return_value=True) + def test_create_group_snap_api_fail(self, *mocks): + self._mock_group.create_snapshot.side_effect = self._raise_infinisdk + self.assertRaises(exception.VolumeBackendAPIException, + self.driver.create_group_snapshot, None, + test_snapgroup, [test_snapshot]) + + @mock.patch('cinder.volume.utils.is_group_a_cg_snapshot_type', + return_value=True) + def test_delete_group_snap(self, *mocks): + self.driver.delete_group_snapshot(None, + test_snapgroup, + [test_snapshot]) + + @mock.patch('cinder.volume.utils.is_group_a_cg_snapshot_type', + return_value=True) + def test_delete_group_snap_does_not_exist(self, *mocks): + self._system.cons_groups.safe_get.return_value = None + self.driver.delete_group_snapshot(None, + test_snapgroup, + [test_snapshot]) + + @mock.patch('cinder.volume.utils.is_group_a_cg_snapshot_type', + return_value=True) + def test_delete_group_snap_invalid_group(self, *mocks): + self._mock_group.is_snapgroup.return_value = False + self.assertRaises(exception.InvalidGroupSnapshot, + self.driver.delete_group_snapshot, + None, test_snapgroup, [test_snapshot]) + + @mock.patch('cinder.volume.utils.is_group_a_cg_snapshot_type', + return_value=True) + def test_delete_group_snap_api_fail(self, *mocks): + self._mock_group.safe_delete.side_effect = self._raise_infinisdk + self.assertRaises(exception.VolumeBackendAPIException, + self.driver.delete_group_snapshot, + None, test_snapgroup, [test_snapshot]) + class InfiniboxDriverTestCaseFC(InfiniboxDriverTestCaseBase): def test_initialize_connection_multiple_wwpns(self): diff --git a/cinder/volume/drivers/infinidat.py b/cinder/volume/drivers/infinidat.py index e6c53a03ec4..999e0c578d6 100644 --- a/cinder/volume/drivers/infinidat.py +++ b/cinder/volume/drivers/infinidat.py @@ -28,6 +28,7 @@ from cinder import coordination from cinder import exception from cinder.i18n import _ from cinder import interface +from cinder.objects import fields from cinder import utils from cinder.volume.drivers.san import san from cinder.volume import utils as vol_utils @@ -88,7 +89,7 @@ def infinisdk_to_cinder_exceptions(func): @interface.volumedriver class InfiniboxVolumeDriver(san.SanISCSIDriver): - VERSION = '1.2' + VERSION = '1.3' # ThirdPartySystems wiki page CI_WIKI_NAME = "INFINIDAT_Cinder_CI" @@ -130,6 +131,12 @@ class InfiniboxVolumeDriver(san.SanISCSIDriver): def _make_host_name(self, port): return 'openstack-host-%s' % str(port).replace(":", ".") + def _make_cg_name(self, cinder_group): + return 'openstack-cg-%s' % cinder_group.id + + def _make_group_snapshot_name(self, cinder_group_snap): + return 'openstack-group-snap-%s' % cinder_group_snap.id + def _get_infinidat_volume_by_name(self, name): volume = self._system.volumes.safe_get(name=name) if volume is None: @@ -163,6 +170,15 @@ class InfiniboxVolumeDriver(san.SanISCSIDriver): raise exception.VolumeDriverException(message=msg) return pool + def _get_infinidat_cg(self, cinder_group): + group_name = self._make_cg_name(cinder_group) + infinidat_cg = self._system.cons_groups.safe_get(name=group_name) + if infinidat_cg is None: + msg = _('Consistency group "%s" not found') % group_name + LOG.error(msg) + raise exception.InvalidGroup(message=msg) + return infinidat_cg + def _get_or_create_host(self, port): host_name = self._make_host_name(port) infinidat_host = self._system.hosts.safe_get(name=host_name) @@ -334,9 +350,10 @@ class InfiniboxVolumeDriver(san.SanISCSIDriver): vendor_name=VENDOR_NAME, driver_version=self.VERSION, storage_protocol=self._protocol, - consistencygroup_support='False', + consistencygroup_support=False, total_capacity_gb=total_capacity_gb, - free_capacity_gb=free_capacity_gb) + free_capacity_gb=free_capacity_gb, + consistent_group_snapshot_enabled=True) return self._volume_stats def _create_volume(self, volume): @@ -538,3 +555,111 @@ class InfiniboxVolumeDriver(san.SanISCSIDriver): init_targ_map[initiator] = target_wwns return target_wwns, init_targ_map + + @infinisdk_to_cinder_exceptions + def create_group(self, context, group): + """Creates a group.""" + # let generic volume group support handle non-cgsnapshots + if not vol_utils.is_group_a_cg_snapshot_type(group): + raise NotImplementedError() + self._system.cons_groups.create(name=self._make_cg_name(group), + pool=self._get_infinidat_pool()) + return {'status': fields.GroupStatus.AVAILABLE} + + @infinisdk_to_cinder_exceptions + def delete_group(self, context, group, volumes): + """Deletes a group.""" + # let generic volume group support handle non-cgsnapshots + if not vol_utils.is_group_a_cg_snapshot_type(group): + raise NotImplementedError() + try: + infinidat_cg = self._get_infinidat_cg(group) + except exception.InvalidGroup: + pass # group not found + else: + infinidat_cg.safe_delete() + for volume in volumes: + self.delete_volume(volume) + return None, None + + @infinisdk_to_cinder_exceptions + def update_group(self, context, group, + add_volumes=None, remove_volumes=None): + """Updates a group.""" + # let generic volume group support handle non-cgsnapshots + if not vol_utils.is_group_a_cg_snapshot_type(group): + raise NotImplementedError() + add_volumes = add_volumes if add_volumes else [] + remove_volumes = remove_volumes if remove_volumes else [] + infinidat_cg = self._get_infinidat_cg(group) + for vol in add_volumes: + infinidat_volume = self._get_infinidat_volume(vol) + infinidat_cg.add_member(infinidat_volume) + for vol in remove_volumes: + infinidat_volume = self._get_infinidat_volume(vol) + infinidat_cg.remove_member(infinidat_volume) + return None, None, None + + @infinisdk_to_cinder_exceptions + def create_group_from_src(self, context, group, volumes, + group_snapshot=None, snapshots=None, + source_group=None, source_vols=None): + """Creates a group from source.""" + # The source is either group_snapshot+snapshots or + # source_group+source_vols. The target is group+voluems + # we assume the source (source_vols / snapshots) are in the same + # order as the target (volumes) + + # let generic volume group support handle non-cgsnapshots + if not vol_utils.is_group_a_cg_snapshot_type(group): + raise NotImplementedError() + self.create_group(context, group) + new_infinidat_group = self._get_infinidat_cg(group) + if group_snapshot is not None and snapshots is not None: + for volume, snapshot in zip(volumes, snapshots): + self.create_volume_from_snapshot(volume, snapshot) + new_infinidat_volume = self._get_infinidat_volume(volume) + new_infinidat_group.add_member(new_infinidat_volume) + elif source_group is not None and source_vols is not None: + for volume, src_vol in zip(volumes, source_vols): + self.create_cloned_volume(volume, src_vol) + new_infinidat_volume = self._get_infinidat_volume(volume) + new_infinidat_group.add_member(new_infinidat_volume) + return None, None + + @infinisdk_to_cinder_exceptions + def create_group_snapshot(self, context, group_snapshot, snapshots): + """Creates a group_snapshot.""" + # let generic volume group support handle non-cgsnapshots + if not vol_utils.is_group_a_cg_snapshot_type(group_snapshot): + raise NotImplementedError() + infinidat_cg = self._get_infinidat_cg(group_snapshot.group) + group_snap_name = self._make_group_snapshot_name(group_snapshot) + new_group = infinidat_cg.create_snapshot(name=group_snap_name) + # update the names of the individual snapshots in the new snapgroup + # to match the names we use for cinder snapshots + for infinidat_snapshot in new_group.get_members(): + parent_name = infinidat_snapshot.get_parent().get_name() + for cinder_snapshot in snapshots: + if cinder_snapshot.volume_id in parent_name: + snapshot_name = self._make_snapshot_name(cinder_snapshot) + infinidat_snapshot.update_name(snapshot_name) + return None, None + + @infinisdk_to_cinder_exceptions + def delete_group_snapshot(self, context, group_snapshot, snapshots): + """Deletes a group_snapshot.""" + # let generic volume group support handle non-cgsnapshots + if not vol_utils.is_group_a_cg_snapshot_type(group_snapshot): + raise NotImplementedError() + cgsnap_name = self._make_group_snapshot_name(group_snapshot) + infinidat_cgsnap = self._system.cons_groups.safe_get(name=cgsnap_name) + if infinidat_cgsnap is not None: + if not infinidat_cgsnap.is_snapgroup(): + msg = _('Group "%s" is not a snapshot group') % cgsnap_name + LOG.error(msg) + raise exception.InvalidGroupSnapshot(message=msg) + infinidat_cgsnap.safe_delete() + for snapshot in snapshots: + self.delete_snapshot(snapshot) + return None, None diff --git a/releasenotes/notes/infinidat-group-support-44cd0715de1ea502.yaml b/releasenotes/notes/infinidat-group-support-44cd0715de1ea502.yaml new file mode 100644 index 00000000000..62f96d6c9cb --- /dev/null +++ b/releasenotes/notes/infinidat-group-support-44cd0715de1ea502.yaml @@ -0,0 +1,3 @@ +--- +features: + - Add CG capability to generic volume groups in INFINIDAT driver.