Merge "Add CG capability to generic groups in VNX driver"
This commit is contained in:
commit
de813e7a74
@ -175,12 +175,12 @@ test_create_snapshot_adapter:
|
|||||||
test_delete_snapshot_adapter:
|
test_delete_snapshot_adapter:
|
||||||
snapshot: *snapshot_base
|
snapshot: *snapshot_base
|
||||||
|
|
||||||
test_create_cgsnapshot: &cg_snap_and_snaps
|
test_do_create_cgsnap: &cg_snap_and_snaps
|
||||||
cg_snap: *cg_snapshot_base
|
cg_snap: *cg_snapshot_base
|
||||||
snap1: *snapshot_base
|
snap1: *snapshot_base
|
||||||
snap2: *snapshot_base
|
snap2: *snapshot_base
|
||||||
|
|
||||||
test_delete_cgsnapshot: *cg_snap_and_snaps
|
test_do_delete_cgsnap: *cg_snap_and_snaps
|
||||||
|
|
||||||
test_manage_existing_lun_no_exist:
|
test_manage_existing_lun_no_exist:
|
||||||
volume: *volume_base
|
volume: *volume_base
|
||||||
@ -244,7 +244,7 @@ test_create_volume_from_snapshot_snapcopy:
|
|||||||
test_get_base_lun_name:
|
test_get_base_lun_name:
|
||||||
volume: *volume_base
|
volume: *volume_base
|
||||||
|
|
||||||
test_create_cg_from_cgsnapshot:
|
test_do_create_cg_from_cgsnap:
|
||||||
vol1:
|
vol1:
|
||||||
_type: 'volume'
|
_type: 'volume'
|
||||||
_properties:
|
_properties:
|
||||||
@ -257,8 +257,6 @@ test_create_cg_from_cgsnapshot:
|
|||||||
<<: *volume_base_properties
|
<<: *volume_base_properties
|
||||||
id:
|
id:
|
||||||
_uuid: volume2_id
|
_uuid: volume2_id
|
||||||
cg: *cg_base
|
|
||||||
cg_snap: *cg_snapshot_base
|
|
||||||
snap1:
|
snap1:
|
||||||
_type: 'snapshot'
|
_type: 'snapshot'
|
||||||
_properties:
|
_properties:
|
||||||
@ -272,21 +270,13 @@ test_create_cg_from_cgsnapshot:
|
|||||||
id:
|
id:
|
||||||
_uuid: snapshot2_id
|
_uuid: snapshot2_id
|
||||||
|
|
||||||
test_create_cloned_cg:
|
test_do_clone_cg:
|
||||||
vol1:
|
vol1:
|
||||||
_type: 'volume'
|
_type: 'volume'
|
||||||
_properties:
|
_properties:
|
||||||
<<: *volume_base_properties
|
<<: *volume_base_properties
|
||||||
id:
|
id:
|
||||||
_uuid: consistency_group_id
|
_uuid: consistency_group_id
|
||||||
cg: *cg_base
|
|
||||||
src_cg:
|
|
||||||
_type: 'cg'
|
|
||||||
_properties:
|
|
||||||
<<: *cg_base_properties
|
|
||||||
id:
|
|
||||||
_uuid: consistency_group2_id
|
|
||||||
name: 'src_cg_name'
|
|
||||||
|
|
||||||
src_vol1:
|
src_vol1:
|
||||||
_type: 'volume'
|
_type: 'volume'
|
||||||
@ -322,7 +312,7 @@ test_remove_host_access_sg_absent:
|
|||||||
test_remove_host_access_volume_not_in_sg:
|
test_remove_host_access_volume_not_in_sg:
|
||||||
volume: *volume_base
|
volume: *volume_base
|
||||||
|
|
||||||
test_update_consistencygroup:
|
test_do_update_cg:
|
||||||
cg: *cg_base
|
cg: *cg_base
|
||||||
volume_add:
|
volume_add:
|
||||||
<<: *volume_base
|
<<: *volume_base
|
||||||
@ -421,13 +411,36 @@ test_update_migrated_volume_smp:
|
|||||||
<<: *provider_location_dict
|
<<: *provider_location_dict
|
||||||
type: smp
|
type: smp
|
||||||
|
|
||||||
|
test_create_group_snap:
|
||||||
|
|
||||||
|
test_create_cgsnapshot:
|
||||||
|
|
||||||
|
test_create_cloned_cg:
|
||||||
|
|
||||||
|
test_create_cloned_group:
|
||||||
|
|
||||||
|
test_create_cg_from_cgsnapshot:
|
||||||
|
|
||||||
|
test_create_group_from_group_snapshot:
|
||||||
|
|
||||||
|
test_create_cgsnapshot:
|
||||||
|
|
||||||
|
test_create_group_snapshot:
|
||||||
|
|
||||||
|
test_delete_group_snapshot:
|
||||||
|
|
||||||
|
test_delete_cgsnapshot:
|
||||||
|
|
||||||
###########################################################
|
###########################################################
|
||||||
# TestUtils
|
# TestUtils
|
||||||
###########################################################
|
###########################################################
|
||||||
|
|
||||||
test_validate_cg_type:
|
test_validate_cg_type:
|
||||||
cg: *cg_base_with_type
|
cg:
|
||||||
|
_properties:
|
||||||
|
id:
|
||||||
|
_uuid: GROUP_ID
|
||||||
|
volume_type_ids: ['type1']
|
||||||
|
|
||||||
|
|
||||||
###########################################################
|
###########################################################
|
||||||
|
@ -150,6 +150,7 @@ mirror_base: &mirror_base
|
|||||||
_type: VNXMirrorImageState
|
_type: VNXMirrorImageState
|
||||||
value: 'SYNCHRONIZED'
|
value: 'SYNCHRONIZED'
|
||||||
|
|
||||||
|
|
||||||
###########################################################
|
###########################################################
|
||||||
# TestClient
|
# TestClient
|
||||||
###########################################################
|
###########################################################
|
||||||
@ -1365,7 +1366,9 @@ test_delete_snapshot_adapter: *test_delete_snapshot
|
|||||||
|
|
||||||
test_create_cgsnapshot: *test_create_cg_snapshot
|
test_create_cgsnapshot: *test_create_cg_snapshot
|
||||||
|
|
||||||
test_delete_cgsnapshot:
|
test_do_create_cgsnap: *test_create_cg_snapshot
|
||||||
|
|
||||||
|
test_do_delete_cgsnap:
|
||||||
cg_snap: &cg_snap_delete
|
cg_snap: &cg_snap_delete
|
||||||
_methods:
|
_methods:
|
||||||
delete:
|
delete:
|
||||||
@ -1373,7 +1376,7 @@ test_delete_cgsnapshot:
|
|||||||
_methods:
|
_methods:
|
||||||
get_snap: *cg_snap_delete
|
get_snap: *cg_snap_delete
|
||||||
|
|
||||||
test_create_cg_from_cgsnapshot:
|
test_do_create_cg_from_cgsnap:
|
||||||
snap: &copied_cg_snap
|
snap: &copied_cg_snap
|
||||||
_methods:
|
_methods:
|
||||||
copy:
|
copy:
|
||||||
@ -1400,7 +1403,7 @@ test_create_cg_from_cgsnapshot:
|
|||||||
get_migration_session: *session_verify
|
get_migration_session: *session_verify
|
||||||
create_cg: *cg_for_create
|
create_cg: *cg_for_create
|
||||||
|
|
||||||
test_create_cloned_cg:
|
test_do_clone_cg:
|
||||||
vnx:
|
vnx:
|
||||||
_properties:
|
_properties:
|
||||||
_methods:
|
_methods:
|
||||||
@ -1773,6 +1776,8 @@ test_terminate_connection_cleanup_sg_is_not_empty:
|
|||||||
|
|
||||||
test_update_consistencygroup:
|
test_update_consistencygroup:
|
||||||
|
|
||||||
|
test_do_update_cg:
|
||||||
|
|
||||||
test_update_migrated_volume:
|
test_update_migrated_volume:
|
||||||
|
|
||||||
test_update_migrated_volume_smp:
|
test_update_migrated_volume_smp:
|
||||||
@ -1785,6 +1790,26 @@ test_normalize_config_iscsi_initiators_empty_str:
|
|||||||
|
|
||||||
test_normalize_config_iscsi_initiators_not_dict:
|
test_normalize_config_iscsi_initiators_not_dict:
|
||||||
|
|
||||||
|
test_create_group_snap:
|
||||||
|
|
||||||
|
test_create_cgsnapshot:
|
||||||
|
|
||||||
|
test_create_cloned_cg:
|
||||||
|
|
||||||
|
test_create_cloned_group:
|
||||||
|
|
||||||
|
test_create_cg_from_cgsnapshot:
|
||||||
|
|
||||||
|
test_create_group_from_group_snapshot:
|
||||||
|
|
||||||
|
test_create_cgsnapshot:
|
||||||
|
|
||||||
|
test_create_group_snapshot:
|
||||||
|
|
||||||
|
test_delete_group_snapshot:
|
||||||
|
|
||||||
|
test_delete_cgsnapshot:
|
||||||
|
|
||||||
|
|
||||||
###########################################################
|
###########################################################
|
||||||
# TestISCSIAdapter
|
# TestISCSIAdapter
|
||||||
|
@ -15,9 +15,12 @@
|
|||||||
import mock
|
import mock
|
||||||
import re
|
import re
|
||||||
|
|
||||||
|
from cinder import context
|
||||||
from cinder import exception
|
from cinder import exception
|
||||||
from cinder.objects import fields
|
from cinder.objects import fields
|
||||||
from cinder import test
|
from cinder import test
|
||||||
|
from cinder.tests.unit import fake_constants
|
||||||
|
from cinder.tests.unit import utils as test_utils
|
||||||
from cinder.tests.unit.volume.drivers.dell_emc.vnx import fake_exception \
|
from cinder.tests.unit.volume.drivers.dell_emc.vnx import fake_exception \
|
||||||
as storops_ex
|
as storops_ex
|
||||||
from cinder.tests.unit.volume.drivers.dell_emc.vnx import fake_storops \
|
from cinder.tests.unit.volume.drivers.dell_emc.vnx import fake_storops \
|
||||||
@ -39,6 +42,7 @@ class TestCommonAdapter(test.TestCase):
|
|||||||
vnx_utils.init_ops(self.configuration)
|
vnx_utils.init_ops(self.configuration)
|
||||||
self.configuration.san_ip = '192.168.1.1'
|
self.configuration.san_ip = '192.168.1.1'
|
||||||
self.configuration.storage_vnx_authentication_type = 'global'
|
self.configuration.storage_vnx_authentication_type = 'global'
|
||||||
|
self.ctxt = context.get_admin_context()
|
||||||
|
|
||||||
def tearDown(self):
|
def tearDown(self):
|
||||||
super(TestCommonAdapter, self).tearDown()
|
super(TestCommonAdapter, self).tearDown()
|
||||||
@ -136,32 +140,124 @@ class TestCommonAdapter(test.TestCase):
|
|||||||
update = vnx_common.create_volume_from_snapshot(volume, snapshot)
|
update = vnx_common.create_volume_from_snapshot(volume, snapshot)
|
||||||
self.assertEqual('True', update['metadata']['snapcopy'])
|
self.assertEqual('True', update['metadata']['snapcopy'])
|
||||||
|
|
||||||
|
@res_mock.patch_common_adapter
|
||||||
|
def test_create_cg_from_cgsnapshot(self, common, _):
|
||||||
|
common.do_create_cg_from_cgsnap = mock.Mock(
|
||||||
|
return_value='fake_return')
|
||||||
|
new_cg = test_utils.create_consistencygroup(
|
||||||
|
self.ctxt,
|
||||||
|
id=fake_constants.CONSISTENCY_GROUP_ID,
|
||||||
|
host='host@backend#unit_test_pool',
|
||||||
|
group_type_id=fake_constants.VOLUME_TYPE_ID)
|
||||||
|
cg_snapshot = test_utils.create_cgsnapshot(
|
||||||
|
self.ctxt,
|
||||||
|
fake_constants.CONSISTENCY_GROUP2_ID)
|
||||||
|
vol = test_utils.create_volume(self.ctxt)
|
||||||
|
snaps = [
|
||||||
|
test_utils.create_snapshot(self.ctxt, vol.id)]
|
||||||
|
vol_new = test_utils.create_volume(self.ctxt)
|
||||||
|
ret = common.create_cg_from_cgsnapshot(
|
||||||
|
None, new_cg, [vol_new], cg_snapshot, snaps)
|
||||||
|
self.assertEqual('fake_return', ret)
|
||||||
|
common.do_create_cg_from_cgsnap.assert_called_once_with(
|
||||||
|
new_cg.id, new_cg.host, [vol_new], cg_snapshot.id, snaps)
|
||||||
|
|
||||||
|
@res_mock.patch_common_adapter
|
||||||
|
def test_create_group_from_group_snapshot(self, common, _):
|
||||||
|
common.do_create_cg_from_cgsnap = mock.Mock(
|
||||||
|
return_value='fake_return')
|
||||||
|
group = test_utils.create_group(
|
||||||
|
self.ctxt,
|
||||||
|
id=fake_constants.CONSISTENCY_GROUP_ID,
|
||||||
|
host='host@backend#unit_test_pool',
|
||||||
|
group_type_id=fake_constants.VOLUME_TYPE_ID)
|
||||||
|
group_snapshot = test_utils.create_group_snapshot(
|
||||||
|
self.ctxt,
|
||||||
|
fake_constants.CGSNAPSHOT_ID,
|
||||||
|
host='host@backend#unit_test_pool',
|
||||||
|
group_type_id=fake_constants.VOLUME_TYPE_ID)
|
||||||
|
vol = test_utils.create_volume(self.ctxt)
|
||||||
|
snaps = [
|
||||||
|
test_utils.create_snapshot(self.ctxt, vol.id)]
|
||||||
|
vol_new = test_utils.create_volume(self.ctxt)
|
||||||
|
ret = common.create_group_from_group_snapshot(
|
||||||
|
None, group, [vol_new], group_snapshot, snaps)
|
||||||
|
self.assertEqual('fake_return', ret)
|
||||||
|
common.do_create_cg_from_cgsnap.assert_called_once_with(
|
||||||
|
group.id, group.host, [vol_new], group_snapshot.id, snaps)
|
||||||
|
|
||||||
@res_mock.mock_driver_input
|
@res_mock.mock_driver_input
|
||||||
@res_mock.patch_common_adapter
|
@res_mock.patch_common_adapter
|
||||||
def test_create_cg_from_cgsnapshot(self, vnx_common, mocked,
|
def test_do_create_cg_from_cgsnap(
|
||||||
cinder_input):
|
self, vnx_common, mocked, cinder_input):
|
||||||
group = cinder_input['cg']
|
cg_id = fake_constants.CONSISTENCY_GROUP_ID
|
||||||
|
cg_host = 'host@backend#unit_test_pool'
|
||||||
volumes = [cinder_input['vol1']]
|
volumes = [cinder_input['vol1']]
|
||||||
cg_snap = cinder_input['cg_snap']
|
cgsnap_id = fake_constants.CGSNAPSHOT_ID
|
||||||
snaps = [cinder_input['snap1']]
|
snaps = [cinder_input['snap1']]
|
||||||
|
|
||||||
model_update, volume_updates = vnx_common.create_cg_from_cgsnapshot(
|
model_update, volume_updates = (
|
||||||
None, group, volumes, cg_snap, snaps)
|
vnx_common.do_create_cg_from_cgsnap(
|
||||||
|
cg_id, cg_host, volumes, cgsnap_id, snaps))
|
||||||
self.assertIsNone(model_update)
|
self.assertIsNone(model_update)
|
||||||
self.assertIsNotNone(
|
self.assertIsNotNone(
|
||||||
re.findall('id^12',
|
re.findall('id^12',
|
||||||
volume_updates[0]['provider_location']))
|
volume_updates[0]['provider_location']))
|
||||||
|
|
||||||
|
@res_mock.patch_common_adapter
|
||||||
|
def test_create_cloned_cg(self, common, _):
|
||||||
|
common.do_clone_cg = mock.Mock(
|
||||||
|
return_value='fake_return')
|
||||||
|
group = test_utils.create_consistencygroup(
|
||||||
|
self.ctxt,
|
||||||
|
id=fake_constants.CONSISTENCY_GROUP_ID,
|
||||||
|
host='host@backend#unit_test_pool',
|
||||||
|
group_type_id=fake_constants.VOLUME_TYPE_ID)
|
||||||
|
src_group = test_utils.create_consistencygroup(
|
||||||
|
self.ctxt,
|
||||||
|
id=fake_constants.CONSISTENCY_GROUP2_ID,
|
||||||
|
host='host@backend#unit_test_pool2',
|
||||||
|
group_type_id=fake_constants.VOLUME_TYPE_ID)
|
||||||
|
vol = test_utils.create_volume(self.ctxt)
|
||||||
|
src_vol = test_utils.create_volume(self.ctxt)
|
||||||
|
ret = common.create_cloned_group(
|
||||||
|
None, group, [vol], src_group, [src_vol])
|
||||||
|
self.assertEqual('fake_return', ret)
|
||||||
|
common.do_clone_cg.assert_called_once_with(
|
||||||
|
group.id, group.host, [vol], src_group.id, [src_vol])
|
||||||
|
|
||||||
|
@res_mock.patch_common_adapter
|
||||||
|
def test_create_cloned_group(self, common, _):
|
||||||
|
common.do_clone_cg = mock.Mock(
|
||||||
|
return_value='fake_return')
|
||||||
|
group = test_utils.create_group(
|
||||||
|
self.ctxt,
|
||||||
|
id=fake_constants.GROUP_ID,
|
||||||
|
host='host@backend#unit_test_pool',
|
||||||
|
group_type_id=fake_constants.VOLUME_TYPE_ID)
|
||||||
|
src_group = test_utils.create_group(
|
||||||
|
self.ctxt,
|
||||||
|
id=fake_constants.GROUP2_ID,
|
||||||
|
host='host@backend#unit_test_pool2',
|
||||||
|
group_type_id=fake_constants.VOLUME_TYPE_ID)
|
||||||
|
vol = test_utils.create_volume(self.ctxt)
|
||||||
|
src_vol = test_utils.create_volume(self.ctxt)
|
||||||
|
ret = common.create_cloned_group(
|
||||||
|
None, group, [vol], src_group, [src_vol])
|
||||||
|
self.assertEqual('fake_return', ret)
|
||||||
|
common.do_clone_cg.assert_called_once_with(
|
||||||
|
group.id, group.host, [vol], src_group.id, [src_vol])
|
||||||
|
|
||||||
@res_mock.mock_driver_input
|
@res_mock.mock_driver_input
|
||||||
@res_mock.patch_common_adapter
|
@res_mock.patch_common_adapter
|
||||||
def test_create_cloned_cg(self, vnx_common, mocked,
|
def test_do_clone_cg(self, vnx_common, _, cinder_input):
|
||||||
cinder_input):
|
cg_id = fake_constants.CONSISTENCY_GROUP_ID
|
||||||
group = cinder_input['cg']
|
cg_host = 'host@backend#unit_test_pool'
|
||||||
src_group = cinder_input['src_cg']
|
|
||||||
volumes = [cinder_input['vol1']]
|
volumes = [cinder_input['vol1']]
|
||||||
|
src_cg_id = fake_constants.CONSISTENCY_GROUP2_ID
|
||||||
src_volumes = [cinder_input['src_vol1']]
|
src_volumes = [cinder_input['src_vol1']]
|
||||||
model_update, volume_updates = vnx_common.create_cloned_cg(
|
model_update, volume_updates = vnx_common.do_clone_cg(
|
||||||
None, group, volumes, src_group, src_volumes)
|
cg_id, cg_host, volumes, src_cg_id, src_volumes)
|
||||||
self.assertIsNone(model_update)
|
self.assertIsNone(model_update)
|
||||||
self.assertIsNotNone(
|
self.assertIsNotNone(
|
||||||
re.findall('id^12',
|
re.findall('id^12',
|
||||||
@ -474,24 +570,105 @@ class TestCommonAdapter(test.TestCase):
|
|||||||
mocked_input):
|
mocked_input):
|
||||||
common_adapter.delete_snapshot(mocked_input['snapshot'])
|
common_adapter.delete_snapshot(mocked_input['snapshot'])
|
||||||
|
|
||||||
|
@res_mock.patch_common_adapter
|
||||||
|
def test_create_cgsnapshot(self, common_adapter, _):
|
||||||
|
common_adapter.do_create_cgsnap = mock.Mock(
|
||||||
|
return_value='fake_return')
|
||||||
|
cg_snapshot = test_utils.create_cgsnapshot(
|
||||||
|
self.ctxt,
|
||||||
|
fake_constants.CONSISTENCY_GROUP_ID)
|
||||||
|
vol = test_utils.create_volume(self.ctxt)
|
||||||
|
snaps = [
|
||||||
|
test_utils.create_snapshot(self.ctxt, vol.id)]
|
||||||
|
ret = common_adapter.create_cgsnapshot(
|
||||||
|
None, cg_snapshot, snaps)
|
||||||
|
self.assertEqual('fake_return', ret)
|
||||||
|
common_adapter.do_create_cgsnap.assert_called_once_with(
|
||||||
|
cg_snapshot.consistencygroup_id,
|
||||||
|
cg_snapshot.id,
|
||||||
|
snaps)
|
||||||
|
|
||||||
|
@res_mock.patch_common_adapter
|
||||||
|
def test_create_group_snap(self, common_adapter, _):
|
||||||
|
common_adapter.do_create_cgsnap = mock.Mock(
|
||||||
|
return_value='fake_return')
|
||||||
|
group_snapshot = test_utils.create_group_snapshot(
|
||||||
|
self.ctxt,
|
||||||
|
fake_constants.GROUP_ID,
|
||||||
|
host='host@backend#unit_test_pool',
|
||||||
|
group_type_id=fake_constants.VOLUME_TYPE_ID)
|
||||||
|
vol = test_utils.create_volume(self.ctxt)
|
||||||
|
snaps = [
|
||||||
|
test_utils.create_snapshot(self.ctxt, vol.id)]
|
||||||
|
ret = common_adapter.create_group_snapshot(
|
||||||
|
None, group_snapshot, snaps)
|
||||||
|
self.assertEqual('fake_return', ret)
|
||||||
|
common_adapter.do_create_cgsnap.assert_called_once_with(
|
||||||
|
group_snapshot.group_id,
|
||||||
|
group_snapshot.id,
|
||||||
|
snaps)
|
||||||
|
|
||||||
@res_mock.mock_driver_input
|
@res_mock.mock_driver_input
|
||||||
@res_mock.patch_common_adapter
|
@res_mock.patch_common_adapter
|
||||||
def test_create_cgsnapshot(self, common_adapter, mocked, mocked_input):
|
def test_do_create_cgsnap(self, common_adapter, _, mocked_input):
|
||||||
cg_snap = mocked_input['cg_snap']
|
group_name = fake_constants.CONSISTENCY_GROUP_ID
|
||||||
|
snap_name = fake_constants.CGSNAPSHOT_ID
|
||||||
snap1 = mocked_input['snap1']
|
snap1 = mocked_input['snap1']
|
||||||
snap2 = mocked_input['snap2']
|
snap2 = mocked_input['snap2']
|
||||||
model_update, snapshots_model_update = (
|
model_update, snapshots_model_update = (
|
||||||
common_adapter.create_cgsnapshot(None, cg_snap, [snap1, snap2]))
|
common_adapter.do_create_cgsnap(group_name, snap_name,
|
||||||
|
[snap1, snap2]))
|
||||||
self.assertEqual('available', model_update['status'])
|
self.assertEqual('available', model_update['status'])
|
||||||
for update in snapshots_model_update:
|
for update in snapshots_model_update:
|
||||||
self.assertEqual(fields.SnapshotStatus.AVAILABLE, update['status'])
|
self.assertEqual(fields.SnapshotStatus.AVAILABLE, update['status'])
|
||||||
|
|
||||||
|
@res_mock.patch_common_adapter
|
||||||
|
def test_delete_group_snapshot(self, common_adapter, _):
|
||||||
|
common_adapter.do_delete_cgsnap = mock.Mock(
|
||||||
|
return_value='fake_return')
|
||||||
|
group_snapshot = test_utils.create_group_snapshot(
|
||||||
|
self.ctxt,
|
||||||
|
fake_constants.GROUP_ID,
|
||||||
|
host='host@backend#unit_test_pool',
|
||||||
|
group_type_id=fake_constants.VOLUME_TYPE_ID)
|
||||||
|
vol = test_utils.create_volume(self.ctxt)
|
||||||
|
snaps = [
|
||||||
|
test_utils.create_snapshot(self.ctxt, vol.id)]
|
||||||
|
ret = common_adapter.delete_group_snapshot(
|
||||||
|
None, group_snapshot, snaps)
|
||||||
|
self.assertEqual('fake_return', ret)
|
||||||
|
common_adapter.do_delete_cgsnap.assert_called_once_with(
|
||||||
|
group_snapshot.group_id,
|
||||||
|
group_snapshot.id,
|
||||||
|
group_snapshot.status,
|
||||||
|
snaps)
|
||||||
|
|
||||||
|
@res_mock.patch_common_adapter
|
||||||
|
def test_delete_cgsnapshot(self, common_adapter, _):
|
||||||
|
common_adapter.do_delete_cgsnap = mock.Mock(
|
||||||
|
return_value='fake_return')
|
||||||
|
cg_snapshot = test_utils.create_cgsnapshot(
|
||||||
|
self.ctxt,
|
||||||
|
fake_constants.CONSISTENCY_GROUP_ID)
|
||||||
|
vol = test_utils.create_volume(self.ctxt)
|
||||||
|
snaps = [
|
||||||
|
test_utils.create_snapshot(self.ctxt, vol.id)]
|
||||||
|
ret = common_adapter.delete_cgsnapshot(None, cg_snapshot, snaps)
|
||||||
|
self.assertEqual('fake_return', ret)
|
||||||
|
common_adapter.do_delete_cgsnap.assert_called_once_with(
|
||||||
|
cg_snapshot.consistencygroup_id,
|
||||||
|
cg_snapshot.id,
|
||||||
|
cg_snapshot.status,
|
||||||
|
snaps)
|
||||||
|
|
||||||
@res_mock.mock_driver_input
|
@res_mock.mock_driver_input
|
||||||
@res_mock.patch_common_adapter
|
@res_mock.patch_common_adapter
|
||||||
def test_delete_cgsnapshot(self, common_adapter, mocked, mocked_input):
|
def test_do_delete_cgsnap(self, common_adapter, _, mocked_input):
|
||||||
|
group_name = fake_constants.CGSNAPSHOT_ID
|
||||||
|
snap_name = fake_constants.CGSNAPSHOT_ID
|
||||||
model_update, snapshot_updates = (
|
model_update, snapshot_updates = (
|
||||||
common_adapter.delete_cgsnapshot(
|
common_adapter.do_delete_cgsnap(
|
||||||
None, mocked_input['cg_snap'],
|
group_name, snap_name, 'available',
|
||||||
[mocked_input['snap1'], mocked_input['snap2']]))
|
[mocked_input['snap1'], mocked_input['snap2']]))
|
||||||
self.assertEqual('deleted', model_update['status'])
|
self.assertEqual('deleted', model_update['status'])
|
||||||
for snap in snapshot_updates:
|
for snap in snapshot_updates:
|
||||||
@ -792,15 +969,13 @@ class TestCommonAdapter(test.TestCase):
|
|||||||
|
|
||||||
@res_mock.mock_driver_input
|
@res_mock.mock_driver_input
|
||||||
@res_mock.patch_common_adapter
|
@res_mock.patch_common_adapter
|
||||||
def test_update_consistencygroup(self, common_adapter, mocked_res,
|
def test_do_update_cg(self, common_adapter, _, mocked_input):
|
||||||
mocked_input):
|
|
||||||
common_adapter.client.update_consistencygroup = mock.Mock()
|
common_adapter.client.update_consistencygroup = mock.Mock()
|
||||||
cg = mocked_input['cg']
|
cg = mocked_input['cg']
|
||||||
common_adapter.client.get_cg = mock.Mock(return_value=cg)
|
common_adapter.client.get_cg = mock.Mock(return_value=cg)
|
||||||
|
common_adapter.do_update_cg(cg.id,
|
||||||
common_adapter.update_consistencygroup(None, cg,
|
[mocked_input['volume_add']],
|
||||||
[mocked_input['volume_add']],
|
[mocked_input['volume_remove']])
|
||||||
[mocked_input['volume_remove']])
|
|
||||||
|
|
||||||
common_adapter.client.update_consistencygroup.assert_called_once_with(
|
common_adapter.client.update_consistencygroup.assert_called_once_with(
|
||||||
cg, [1], [2])
|
cg, [1], [2])
|
||||||
|
@ -71,3 +71,11 @@ class TestVNXDriver(test.TestCase):
|
|||||||
_driver.terminate_connection('fake_volume', {'host': 'fake_host'})
|
_driver.terminate_connection('fake_volume', {'host': 'fake_host'})
|
||||||
_driver.adapter.terminate_connection.assert_called_once_with(
|
_driver.adapter.terminate_connection.assert_called_once_with(
|
||||||
'fake_volume', {'host': 'fake_host'})
|
'fake_volume', {'host': 'fake_host'})
|
||||||
|
|
||||||
|
def test_is_consistent_group_snapshot_enabled(self):
|
||||||
|
_driver = self._get_driver('iscsi')
|
||||||
|
_driver._stats = {'consistent_group_snapshot_enabled': True}
|
||||||
|
self.assertTrue(_driver.is_consistent_group_snapshot_enabled())
|
||||||
|
_driver._stats = {'consistent_group_snapshot_enabled': False}
|
||||||
|
self.assertFalse(_driver.is_consistent_group_snapshot_enabled())
|
||||||
|
self.assertFalse(_driver.is_consistent_group_snapshot_enabled())
|
||||||
|
@ -27,6 +27,18 @@ from cinder.volume.drivers.dell_emc.vnx import common
|
|||||||
from cinder.volume.drivers.dell_emc.vnx import utils
|
from cinder.volume.drivers.dell_emc.vnx import utils
|
||||||
|
|
||||||
|
|
||||||
|
class FakeDriver(object):
|
||||||
|
def __init__(self, support):
|
||||||
|
self.support = support
|
||||||
|
|
||||||
|
def is_consistent_group_snapshot_enabled(self):
|
||||||
|
return self.support
|
||||||
|
|
||||||
|
@utils.require_consistent_group_snapshot_enabled
|
||||||
|
def fake_method(self):
|
||||||
|
return 'called'
|
||||||
|
|
||||||
|
|
||||||
class TestUtils(test.TestCase):
|
class TestUtils(test.TestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
super(TestUtils, self).setUp()
|
super(TestUtils, self).setUp()
|
||||||
@ -173,3 +185,14 @@ class TestUtils(test.TestCase):
|
|||||||
'wwn2_1': ['wwnt_1', 'wwnt_3'],
|
'wwn2_1': ['wwnt_1', 'wwnt_3'],
|
||||||
'wwn2_2': ['wwnt_1', 'wwnt_3']},
|
'wwn2_2': ['wwnt_1', 'wwnt_3']},
|
||||||
itor_tgt_map)
|
itor_tgt_map)
|
||||||
|
|
||||||
|
def test_cg_snapshot_is_not_enabled(self):
|
||||||
|
def do():
|
||||||
|
driver = FakeDriver(False)
|
||||||
|
driver.fake_method()
|
||||||
|
self.assertRaises(NotImplementedError, do)
|
||||||
|
|
||||||
|
def test_cg_snapshot_is_enabled(self):
|
||||||
|
driver = FakeDriver(True)
|
||||||
|
ret = driver.fake_method()
|
||||||
|
self.assertEqual('called', ret)
|
||||||
|
@ -222,9 +222,10 @@ class CommonAdapter(object):
|
|||||||
'provision': provision,
|
'provision': provision,
|
||||||
'tier': tier})
|
'tier': tier})
|
||||||
|
|
||||||
|
cg_id = volume.group_id or volume.consistencygroup_id
|
||||||
lun = self.client.create_lun(
|
lun = self.client.create_lun(
|
||||||
pool, volume_name, volume_size,
|
pool, volume_name, volume_size,
|
||||||
provision, tier, volume.consistencygroup_id,
|
provision, tier, cg_id,
|
||||||
ignore_thresholds=self.config.ignore_pool_full_threshold)
|
ignore_thresholds=self.config.ignore_pool_full_threshold)
|
||||||
location = self._build_provider_location(
|
location = self._build_provider_location(
|
||||||
lun_type='lun',
|
lun_type='lun',
|
||||||
@ -464,15 +465,21 @@ class CommonAdapter(object):
|
|||||||
return model_update, volumes_model_update
|
return model_update, volumes_model_update
|
||||||
|
|
||||||
def create_cgsnapshot(self, context, cgsnapshot, snapshots):
|
def create_cgsnapshot(self, context, cgsnapshot, snapshots):
|
||||||
|
|
||||||
"""Creates a CG snapshot(snap group)."""
|
"""Creates a CG snapshot(snap group)."""
|
||||||
|
return self.do_create_cgsnap(cgsnapshot.consistencygroup_id,
|
||||||
|
cgsnapshot.id,
|
||||||
|
snapshots)
|
||||||
|
|
||||||
|
def do_create_cgsnap(self, group_name, snap_name, snapshots):
|
||||||
model_update = {}
|
model_update = {}
|
||||||
snapshots_model_update = []
|
snapshots_model_update = []
|
||||||
LOG.info(_LI('Creating CG snapshot for consistency group'
|
LOG.info(_LI('Creating consistency snapshot for group'
|
||||||
': %(group_name)s'),
|
': %(group_name)s'),
|
||||||
{'group_name': cgsnapshot.consistencygroup_id})
|
{'group_name': group_name})
|
||||||
|
|
||||||
self.client.create_cg_snapshot(cgsnapshot.id,
|
self.client.create_cg_snapshot(snap_name,
|
||||||
cgsnapshot.consistencygroup_id)
|
group_name)
|
||||||
for snapshot in snapshots:
|
for snapshot in snapshots:
|
||||||
snapshots_model_update.append(
|
snapshots_model_update.append(
|
||||||
{'id': snapshot.id, 'status': 'available'})
|
{'id': snapshot.id, 'status': 'available'})
|
||||||
@ -482,15 +489,22 @@ class CommonAdapter(object):
|
|||||||
|
|
||||||
def delete_cgsnapshot(self, context, cgsnapshot, snapshots):
|
def delete_cgsnapshot(self, context, cgsnapshot, snapshots):
|
||||||
"""Deletes a CG snapshot(snap group)."""
|
"""Deletes a CG snapshot(snap group)."""
|
||||||
|
return self.do_delete_cgsnap(cgsnapshot.consistencygroup_id,
|
||||||
|
cgsnapshot.id,
|
||||||
|
cgsnapshot.status,
|
||||||
|
snapshots)
|
||||||
|
|
||||||
|
def do_delete_cgsnap(self, group_name, snap_name,
|
||||||
|
snap_status, snapshots):
|
||||||
model_update = {}
|
model_update = {}
|
||||||
snapshots_model_update = []
|
snapshots_model_update = []
|
||||||
model_update['status'] = cgsnapshot.status
|
model_update['status'] = snap_status
|
||||||
LOG.info(_LI('Deleting CG snapshot %(snap_name)s for consistency '
|
LOG.info(_LI('Deleting consistency snapshot %(snap_name)s for '
|
||||||
'group: %(group_name)s'),
|
'group: %(group_name)s'),
|
||||||
{'snap_name': cgsnapshot.id,
|
{'snap_name': snap_name,
|
||||||
'group_name': cgsnapshot.consistencygroup_id})
|
'group_name': group_name})
|
||||||
|
|
||||||
self.client.delete_cg_snapshot(cgsnapshot.id)
|
self.client.delete_cg_snapshot(snap_name)
|
||||||
for snapshot in snapshots:
|
for snapshot in snapshots:
|
||||||
snapshots_model_update.append(
|
snapshots_model_update.append(
|
||||||
{'id': snapshot.id, 'status': 'deleted'})
|
{'id': snapshot.id, 'status': 'deleted'})
|
||||||
@ -500,6 +514,11 @@ class CommonAdapter(object):
|
|||||||
|
|
||||||
def create_cg_from_cgsnapshot(self, context, group,
|
def create_cg_from_cgsnapshot(self, context, group,
|
||||||
volumes, cgsnapshot, snapshots):
|
volumes, cgsnapshot, snapshots):
|
||||||
|
return self.do_create_cg_from_cgsnap(
|
||||||
|
group.id, group.host, volumes, cgsnapshot.id, snapshots)
|
||||||
|
|
||||||
|
def do_create_cg_from_cgsnap(self, cg_id, cg_host, volumes,
|
||||||
|
cgsnap_id, snapshots):
|
||||||
# 1. Copy a temp CG snapshot from CG snapshot
|
# 1. Copy a temp CG snapshot from CG snapshot
|
||||||
# and allow RW for it
|
# and allow RW for it
|
||||||
# 2. Create SMPs from source volumes
|
# 2. Create SMPs from source volumes
|
||||||
@ -509,9 +528,9 @@ class CommonAdapter(object):
|
|||||||
# 6. Wait completion of migration
|
# 6. Wait completion of migration
|
||||||
# 7. Create a new CG, add all LUNs to it
|
# 7. Create a new CG, add all LUNs to it
|
||||||
# 8. Delete the temp CG snapshot
|
# 8. Delete the temp CG snapshot
|
||||||
cg_name = group.id
|
cg_name = cg_id
|
||||||
src_cg_snap_name = cgsnapshot.id
|
src_cg_snap_name = cgsnap_id
|
||||||
pool_name = utils.get_pool_from_host(group.host)
|
pool_name = utils.get_pool_from_host(cg_host)
|
||||||
lun_sizes = []
|
lun_sizes = []
|
||||||
lun_names = []
|
lun_names = []
|
||||||
src_lun_names = []
|
src_lun_names = []
|
||||||
@ -549,9 +568,14 @@ class CommonAdapter(object):
|
|||||||
|
|
||||||
def create_cloned_cg(self, context, group,
|
def create_cloned_cg(self, context, group,
|
||||||
volumes, source_cg, source_vols):
|
volumes, source_cg, source_vols):
|
||||||
|
self.do_clone_cg(group.id, group.host, volumes,
|
||||||
|
source_cg.id, source_vols)
|
||||||
|
|
||||||
|
def do_clone_cg(self, cg_id, cg_host, volumes,
|
||||||
|
source_cg_id, source_vols):
|
||||||
# 1. Create temp CG snapshot from source_cg
|
# 1. Create temp CG snapshot from source_cg
|
||||||
# Same with steps 2-8 of create_cg_from_cgsnapshot
|
# Same with steps 2-8 of create_cg_from_cgsnapshot
|
||||||
pool_name = utils.get_pool_from_host(group.host)
|
pool_name = utils.get_pool_from_host(cg_host)
|
||||||
lun_sizes = []
|
lun_sizes = []
|
||||||
lun_names = []
|
lun_names = []
|
||||||
src_lun_names = []
|
src_lun_names = []
|
||||||
@ -564,8 +588,8 @@ class CommonAdapter(object):
|
|||||||
|
|
||||||
lun_id_list = emc_taskflow.create_cloned_cg(
|
lun_id_list = emc_taskflow.create_cloned_cg(
|
||||||
client=self.client,
|
client=self.client,
|
||||||
cg_name=group.id,
|
cg_name=cg_id,
|
||||||
src_cg_name=source_cg.id,
|
src_cg_name=source_cg_id,
|
||||||
pool_name=pool_name,
|
pool_name=pool_name,
|
||||||
lun_sizes=lun_sizes,
|
lun_sizes=lun_sizes,
|
||||||
lun_names=lun_names,
|
lun_names=lun_names,
|
||||||
@ -623,6 +647,8 @@ class CommonAdapter(object):
|
|||||||
stats['thin_provisioning_support'] = self.client.is_thin_enabled()
|
stats['thin_provisioning_support'] = self.client.is_thin_enabled()
|
||||||
stats['consistencygroup_support'] = self.client.is_snap_enabled()
|
stats['consistencygroup_support'] = self.client.is_snap_enabled()
|
||||||
stats['replication_enabled'] = True if self.mirror_view else False
|
stats['replication_enabled'] = True if self.mirror_view else False
|
||||||
|
stats['consistent_group_snapshot_enabled'] = (
|
||||||
|
self.client.is_snap_enabled())
|
||||||
return stats
|
return stats
|
||||||
|
|
||||||
def get_pool_stats(self, enabler_stats=None):
|
def get_pool_stats(self, enabler_stats=None):
|
||||||
@ -1017,7 +1043,12 @@ class CommonAdapter(object):
|
|||||||
|
|
||||||
def update_consistencygroup(self, context, group, add_volumes,
|
def update_consistencygroup(self, context, group, add_volumes,
|
||||||
remove_volumes):
|
remove_volumes):
|
||||||
cg = self.client.get_cg(name=group.id)
|
return self.do_update_cg(group.id, add_volumes,
|
||||||
|
remove_volumes)
|
||||||
|
|
||||||
|
def do_update_cg(self, cg_name, add_volumes,
|
||||||
|
remove_volumes):
|
||||||
|
cg = self.client.get_cg(name=cg_name)
|
||||||
lun_ids_to_add = [self.client.get_lun_id(volume)
|
lun_ids_to_add = [self.client.get_lun_id(volume)
|
||||||
for volume in add_volumes]
|
for volume in add_volumes]
|
||||||
lun_ids_to_remove = [self.client.get_lun_id(volume)
|
lun_ids_to_remove = [self.client.get_lun_id(volume)
|
||||||
@ -1204,6 +1235,46 @@ class CommonAdapter(object):
|
|||||||
return {'provider_location': new_volume.provider_location,
|
return {'provider_location': new_volume.provider_location,
|
||||||
'metadata': metadata}
|
'metadata': metadata}
|
||||||
|
|
||||||
|
def create_group(self, context, group):
|
||||||
|
return self.create_consistencygroup(context, group)
|
||||||
|
|
||||||
|
def delete_group(self, context, group, volumes):
|
||||||
|
return self.delete_consistencygroup(context, group, volumes)
|
||||||
|
|
||||||
|
def create_group_snapshot(self, context, group_snapshot, snapshots):
|
||||||
|
"""Creates a group_snapshot."""
|
||||||
|
return self.do_create_cgsnap(group_snapshot.group_id,
|
||||||
|
group_snapshot.id,
|
||||||
|
snapshots)
|
||||||
|
|
||||||
|
def delete_group_snapshot(self, context, group_snapshot, snapshots):
|
||||||
|
"""Deletes a group snapshot."""
|
||||||
|
return self.do_delete_cgsnap(
|
||||||
|
group_snapshot.group_id,
|
||||||
|
group_snapshot.id,
|
||||||
|
group_snapshot.status,
|
||||||
|
snapshots)
|
||||||
|
|
||||||
|
def create_group_from_group_snapshot(self,
|
||||||
|
context, group, volumes,
|
||||||
|
group_snapshot, snapshots):
|
||||||
|
"""Creates a group from a group snapshot."""
|
||||||
|
return self.do_create_cg_from_cgsnap(group.id, group.host, volumes,
|
||||||
|
group_snapshot.id, snapshots)
|
||||||
|
|
||||||
|
def update_group(self, context, group,
|
||||||
|
add_volumes=None, remove_volumes=None):
|
||||||
|
"""Updates a group."""
|
||||||
|
return self.do_update_cg(group.id,
|
||||||
|
add_volumes,
|
||||||
|
remove_volumes)
|
||||||
|
|
||||||
|
def create_cloned_group(self, context, group, volumes,
|
||||||
|
source_group, source_vols):
|
||||||
|
"""Clones a group"""
|
||||||
|
return self.do_clone_cg(group.id, group.host, volumes,
|
||||||
|
source_group.id, source_vols)
|
||||||
|
|
||||||
|
|
||||||
class ISCSIAdapter(CommonAdapter):
|
class ISCSIAdapter(CommonAdapter):
|
||||||
def __init__(self, configuration, active_backend_id):
|
def __init__(self, configuration, active_backend_id):
|
||||||
|
@ -86,6 +86,7 @@ class VNXDriver(driver.TransferVD,
|
|||||||
self.protocol = self.configuration.storage_protocol.lower()
|
self.protocol = self.configuration.storage_protocol.lower()
|
||||||
self.active_backend_id = kwargs.get('active_backend_id', None)
|
self.active_backend_id = kwargs.get('active_backend_id', None)
|
||||||
self.adapter = None
|
self.adapter = None
|
||||||
|
self._stats = {}
|
||||||
|
|
||||||
def do_setup(self, context):
|
def do_setup(self, context):
|
||||||
if self.protocol == common.PROTOCOL_FC:
|
if self.protocol == common.PROTOCOL_FC:
|
||||||
@ -331,3 +332,49 @@ class VNXDriver(driver.TransferVD,
|
|||||||
def failover_host(self, context, volumes, secondary_id=None):
|
def failover_host(self, context, volumes, secondary_id=None):
|
||||||
"""Fail-overs volumes from primary device to secondary."""
|
"""Fail-overs volumes from primary device to secondary."""
|
||||||
return self.adapter.failover_host(context, volumes, secondary_id)
|
return self.adapter.failover_host(context, volumes, secondary_id)
|
||||||
|
|
||||||
|
@utils.require_consistent_group_snapshot_enabled
|
||||||
|
def create_group(self, context, group):
|
||||||
|
"""Creates a group."""
|
||||||
|
return self.adapter.create_group(context, group)
|
||||||
|
|
||||||
|
@utils.require_consistent_group_snapshot_enabled
|
||||||
|
def delete_group(self, context, group, volumes):
|
||||||
|
"""Deletes a group."""
|
||||||
|
return self.adapter.delete_group(
|
||||||
|
context, group, volumes)
|
||||||
|
|
||||||
|
@utils.require_consistent_group_snapshot_enabled
|
||||||
|
def update_group(self, context, group,
|
||||||
|
add_volumes=None, remove_volumes=None):
|
||||||
|
"""Updates a group."""
|
||||||
|
return self.adapter.update_group(context, group,
|
||||||
|
add_volumes,
|
||||||
|
remove_volumes)
|
||||||
|
|
||||||
|
@utils.require_consistent_group_snapshot_enabled
|
||||||
|
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."""
|
||||||
|
if group_snapshot:
|
||||||
|
return self.adapter.create_group_from_group_snapshot(
|
||||||
|
context, group, volumes, group_snapshot, snapshots)
|
||||||
|
elif source_group:
|
||||||
|
return self.adapter.create_cloned_group(
|
||||||
|
context, group, volumes, source_group, source_vols)
|
||||||
|
|
||||||
|
@utils.require_consistent_group_snapshot_enabled
|
||||||
|
def create_group_snapshot(self, context, group_snapshot, snapshots):
|
||||||
|
"""Creates a group_snapshot."""
|
||||||
|
return self.adapter.create_group_snapshot(
|
||||||
|
context, group_snapshot, snapshots)
|
||||||
|
|
||||||
|
@utils.require_consistent_group_snapshot_enabled
|
||||||
|
def delete_group_snapshot(self, context, group_snapshot, snapshots):
|
||||||
|
"""Deletes a group_snapshot."""
|
||||||
|
return self.adapter.delete_group_snapshot(
|
||||||
|
context, group_snapshot, snapshots)
|
||||||
|
|
||||||
|
def is_consistent_group_snapshot_enabled(self):
|
||||||
|
return self._stats.get('consistent_group_snapshot_enabled')
|
||||||
|
@ -246,9 +246,9 @@ def get_migration_rate(volume):
|
|||||||
|
|
||||||
|
|
||||||
def validate_cg_type(group):
|
def validate_cg_type(group):
|
||||||
if group.get('volume_type_id') is None:
|
if not group.get('volume_type_ids'):
|
||||||
return
|
return
|
||||||
for type_id in group['volume_type_id'].split(","):
|
for type_id in group.get('volume_type_ids'):
|
||||||
if type_id:
|
if type_id:
|
||||||
specs = volume_types.get_volume_type_extra_specs(type_id)
|
specs = volume_types.get_volume_type_extra_specs(type_id)
|
||||||
extra_specs = common.ExtraSpecs(specs)
|
extra_specs = common.ExtraSpecs(specs)
|
||||||
@ -337,3 +337,12 @@ def truncate_fc_port_wwn(wwn):
|
|||||||
|
|
||||||
def is_volume_smp(volume):
|
def is_volume_smp(volume):
|
||||||
return 'smp' == extract_provider_location(volume.provider_location, 'type')
|
return 'smp' == extract_provider_location(volume.provider_location, 'type')
|
||||||
|
|
||||||
|
|
||||||
|
def require_consistent_group_snapshot_enabled(func):
|
||||||
|
@six.wraps(func)
|
||||||
|
def inner(self, *args, **kwargs):
|
||||||
|
if not self.is_consistent_group_snapshot_enabled():
|
||||||
|
raise NotImplementedError
|
||||||
|
return func(self, *args, **kwargs)
|
||||||
|
return inner
|
||||||
|
@ -0,0 +1,3 @@
|
|||||||
|
---
|
||||||
|
features:
|
||||||
|
- Add consistent group capability to generic volume groups in VNX driver.
|
Loading…
Reference in New Issue
Block a user