Merge "Add support to PureISCSIDriver for Consistency Groups"

This commit is contained in:
Jenkins 2014-12-29 21:16:55 +00:00 committed by Gerrit Code Review
commit 68613e255b
2 changed files with 410 additions and 3 deletions

View File

@ -49,7 +49,11 @@ VOLUME = {"name": "volume-" + VOLUME_ID,
"host": "irrelevant",
"volume_type": None,
"volume_type_id": None,
"consistencygroup_id": None
}
VOLUME_WITH_CGROUP = VOLUME.copy()
VOLUME_WITH_CGROUP['consistencygroup_id'] = \
"4a2f7e3a-312a-40c5-96a8-536b8a0fe074"
SRC_VOL_ID = "dc7a294d-5964-4379-a15f-ce5554734efc"
SRC_VOL = {"name": "volume-" + SRC_VOL_ID,
"id": SRC_VOL_ID,
@ -58,6 +62,7 @@ SRC_VOL = {"name": "volume-" + SRC_VOL_ID,
"host": "irrelevant",
"volume_type": None,
"volume_type_id": None,
"consistencygroup_id": None
}
SNAPSHOT_ID = "04fe2f9a-d0c4-4564-a30d-693cc3657b47"
SNAPSHOT = {"name": "snapshot-" + SNAPSHOT_ID,
@ -66,7 +71,11 @@ SNAPSHOT = {"name": "snapshot-" + SNAPSHOT_ID,
"volume_name": "volume-" + SRC_VOL_ID,
"volume_size": 2,
"display_name": "fake_snapshot",
"cgsnapshot_id": None
}
SNAPSHOT_WITH_CGROUP = SNAPSHOT.copy()
SNAPSHOT_WITH_CGROUP['cgsnapshot_id'] = \
"4a2f7e3a-312a-40c5-96a8-536b8a0fe075"
INITIATOR_IQN = "iqn.1993-08.org.debian:01:222"
CONNECTOR = {"initiator": INITIATOR_IQN, "host": HOSTNAME}
TARGET_IQN = "iqn.2010-06.com.purestorage:flasharray.12345abc"
@ -155,9 +164,22 @@ class PureISCSIDriverTestCase(test.TestCase):
self.assert_error_propagates([self.array.create_volume],
self.driver.create_volume, VOLUME)
@mock.patch(DRIVER_OBJ + "._add_volume_to_consistency_group",
autospec=True)
def test_create_volume_with_cgroup(self, mock_add_to_cgroup):
vol_name = VOLUME_WITH_CGROUP["name"] + "-cinder"
self.driver.create_volume(VOLUME_WITH_CGROUP)
mock_add_to_cgroup\
.assert_called_with(self.driver,
VOLUME_WITH_CGROUP['consistencygroup_id'],
vol_name)
def test_create_volume_from_snapshot(self):
vol_name = VOLUME["name"] + "-cinder"
snap_name = SNAPSHOT["volume_name"] + "-cinder." + SNAPSHOT["name"]
# Branch where extend unneeded
self.driver.create_volume_from_snapshot(VOLUME, SNAPSHOT)
self.array.copy_volume.assert_called_with(snap_name, vol_name)
@ -166,6 +188,7 @@ class PureISCSIDriverTestCase(test.TestCase):
[self.array.copy_volume],
self.driver.create_volume_from_snapshot, VOLUME, SNAPSHOT)
self.assertFalse(self.array.extend_volume.called)
# Branch where extend needed
SNAPSHOT["volume_size"] = 1 # resize so smaller than VOLUME
self.driver.create_volume_from_snapshot(VOLUME, SNAPSHOT)
@ -177,6 +200,33 @@ class PureISCSIDriverTestCase(test.TestCase):
self.driver.create_volume_from_snapshot, VOLUME, SNAPSHOT)
SNAPSHOT["volume_size"] = 2 # reset size
@mock.patch(DRIVER_OBJ + "._add_volume_to_consistency_group",
autospec=True)
@mock.patch(DRIVER_OBJ + "._extend_if_needed", autospec=True)
@mock.patch(DRIVER_PATH + "._get_pgroup_vol_snap_name", autospec=True)
def test_create_volume_from_cgsnapshot(self, mock_get_snap_name,
mock_extend_if_needed,
mock_add_to_cgroup):
vol_name = VOLUME_WITH_CGROUP["name"] + "-cinder"
snap_name = "consisgroup-4a2f7e3a-312a-40c5-96a8-536b8a0f" \
"e074-cinder.4a2f7e3a-312a-40c5-96a8-536b8a0fe075."\
+ vol_name
mock_get_snap_name.return_value = snap_name
self.driver.create_volume_from_snapshot(VOLUME_WITH_CGROUP,
SNAPSHOT_WITH_CGROUP)
self.array.copy_volume.assert_called_with(snap_name, vol_name)
self.assertTrue(mock_get_snap_name.called)
self.assertTrue(mock_extend_if_needed.called)
self.driver.create_volume_from_snapshot(VOLUME_WITH_CGROUP,
SNAPSHOT_WITH_CGROUP)
mock_add_to_cgroup\
.assert_called_with(self.driver,
VOLUME_WITH_CGROUP['consistencygroup_id'],
vol_name)
def test_create_cloned_volume(self):
vol_name = VOLUME["name"] + "-cinder"
src_name = SRC_VOL["name"] + "-cinder"
@ -199,6 +249,18 @@ class PureISCSIDriverTestCase(test.TestCase):
self.driver.create_cloned_volume, VOLUME, SRC_VOL)
SRC_VOL["size"] = 2 # reset size
@mock.patch(DRIVER_OBJ + "._add_volume_to_consistency_group",
autospec=True)
def test_create_cloned_volume_with_cgroup(self, mock_add_to_cgroup):
vol_name = VOLUME_WITH_CGROUP["name"] + "-cinder"
self.driver.create_cloned_volume(VOLUME_WITH_CGROUP, SRC_VOL)
mock_add_to_cgroup\
.assert_called_with(self.driver,
VOLUME_WITH_CGROUP['consistencygroup_id'],
vol_name)
def test_delete_volume_already_deleted(self):
self.array.list_volume_hosts.side_effect = exception.PureAPIException(
code=400, reason="Volume does not exist")
@ -468,6 +530,7 @@ class PureISCSIDriverTestCase(test.TestCase):
"total_capacity_gb": TOTAL_SPACE,
"free_capacity_gb": FREE_SPACE,
"reserved_percentage": 0,
"consistencygroup_support": True
}
real_result = self.driver.get_volume_stats(refresh=True)
self.assertDictMatch(result, real_result)
@ -480,6 +543,210 @@ class PureISCSIDriverTestCase(test.TestCase):
self.assert_error_propagates([self.array.extend_volume],
self.driver.extend_volume, VOLUME, 3)
def test_get_pgroup_name_from_id(self):
id = "4a2f7e3a-312a-40c5-96a8-536b8a0fe074"
expected_name = "consisgroup-%s-cinder" % id
actual_name = pure._get_pgroup_name_from_id(id)
self.assertEqual(expected_name, actual_name)
def test_get_pgroup_snap_suffix(self):
cgsnap = mock.Mock()
cgsnap.id = "4a2f7e3a-312a-40c5-96a8-536b8a0fe074"
expected_suffix = "cgsnapshot-%s-cinder" % cgsnap.id
actual_suffix = pure._get_pgroup_snap_suffix(cgsnap)
self.assertEqual(expected_suffix, actual_suffix)
def test_get_pgroup_snap_name(self):
cg_id = "4a2f7e3a-312a-40c5-96a8-536b8a0fe074"
cgsnap_id = "4a2f7e3a-312a-40c5-96a8-536b8a0fe075"
mock_cgsnap = mock.Mock()
mock_cgsnap.consistencygroup_id = cg_id
mock_cgsnap.id = cgsnap_id
expected_name = "consisgroup-%(cg)s-cinder.cgsnapshot-%(snap)s-cinder"\
% {"cg": cg_id, "snap": cgsnap_id}
actual_name = pure._get_pgroup_snap_name(mock_cgsnap)
self.assertEqual(expected_name, actual_name)
def test_get_pgroup_vol_snap_name(self):
cg_id = "4a2f7e3a-312a-40c5-96a8-536b8a0fe074"
cgsnap_id = "4a2f7e3a-312a-40c5-96a8-536b8a0fe075"
volume_name = "volume-4a2f7e3a-312a-40c5-96a8-536b8a0fe075"
mock_snap = mock.Mock()
mock_snap.cgsnapshot = mock.Mock()
mock_snap.cgsnapshot.consistencygroup_id = cg_id
mock_snap.cgsnapshot.id = cgsnap_id
mock_snap.volume_name = volume_name
expected_name = "consisgroup-%(cg)s-cinder.cgsnapshot-%(snap)s-cinder"\
".%(vol)s-cinder" % {"cg": cg_id,
"snap": cgsnap_id,
"vol": volume_name}
actual_name = pure._get_pgroup_vol_snap_name(mock_snap)
self.assertEqual(expected_name, actual_name)
def test_create_consistencygroup(self):
mock_cgroup = mock.Mock()
mock_cgroup.id = "4a2f7e3a-312a-40c5-96a8-536b8a0fe074"
model_update = self.driver.create_consistencygroup(None, mock_cgroup)
expected_name = pure._get_pgroup_name_from_id(mock_cgroup.id)
self.array.create_pgroup.assert_called_with(expected_name)
self.assertEqual({'status': 'available'}, model_update)
self.assert_error_propagates(
[self.array.create_pgroup],
self.driver.create_consistencygroup, None, mock_cgroup)
@mock.patch(DRIVER_OBJ + ".delete_volume", autospec=True)
def test_delete_consistencygroup(self, mock_delete_volume):
mock_cgroup = mock.MagicMock()
mock_cgroup.id = "4a2f7e3a-312a-40c5-96a8-536b8a0fe074"
mock_cgroup['status'] = "deleted"
mock_context = mock.Mock()
self.driver.db = mock.Mock()
mock_volume = mock.MagicMock()
expected_volumes = [mock_volume]
self.driver.db.volume_get_all_by_group.return_value = expected_volumes
model_update, volumes = \
self.driver.delete_consistencygroup(mock_context, mock_cgroup)
expected_name = pure._get_pgroup_name_from_id(mock_cgroup.id)
self.array.delete_pgroup.assert_called_with(expected_name)
self.assertEqual(expected_volumes, volumes)
self.assertEqual(mock_cgroup['status'], model_update['status'])
mock_delete_volume.assert_called_with(self.driver, mock_volume)
self.array.delete_pgroup.side_effect = exception.PureAPIException(
code=400, reason="Protection group has been destroyed.")
self.driver.delete_consistencygroup(mock_context, mock_cgroup)
self.array.delete_pgroup.assert_called_with(expected_name)
mock_delete_volume.assert_called_with(self.driver, mock_volume)
self.array.delete_pgroup.side_effect = exception.PureAPIException(
code=400, reason="Protection group does not exist")
self.driver.delete_consistencygroup(mock_context, mock_cgroup)
self.array.delete_pgroup.assert_called_with(expected_name)
mock_delete_volume.assert_called_with(self.driver, mock_volume)
self.array.delete_pgroup.side_effect = exception.PureAPIException(
code=400, reason="Some other error")
self.assertRaises(exception.PureAPIException,
self.driver.delete_consistencygroup,
mock_context,
mock_volume)
self.array.delete_pgroup.side_effect = exception.PureAPIException(
code=500, reason="Another different error")
self.assertRaises(exception.PureAPIException,
self.driver.delete_consistencygroup,
mock_context,
mock_volume)
self.array.delete_pgroup.side_effect = None
self.assert_error_propagates(
[self.array.delete_pgroup],
self.driver.delete_consistencygroup, mock_context, mock_cgroup)
def test_create_cgsnapshot(self):
mock_cgsnap = mock.Mock()
mock_cgsnap.id = "4a2f7e3a-312a-40c5-96a8-536b8a0fe074"
mock_cgsnap.consistencygroup_id = \
"4a2f7e3a-312a-40c5-96a8-536b8a0fe075"
mock_context = mock.Mock()
self.driver.db = mock.Mock()
mock_snap = mock.MagicMock()
expected_snaps = [mock_snap]
self.driver.db.snapshot_get_all_for_cgsnapshot.return_value = \
expected_snaps
model_update, snapshots = \
self.driver.create_cgsnapshot(mock_context, mock_cgsnap)
expected_pgroup_name = \
pure._get_pgroup_name_from_id(mock_cgsnap.consistencygroup_id)
expected_snap_suffix = pure._get_pgroup_snap_suffix(mock_cgsnap)
self.array.create_pgroup_snapshot\
.assert_called_with(expected_pgroup_name, expected_snap_suffix)
self.assertEqual({'status': 'available'}, model_update)
self.assertEqual(expected_snaps, snapshots)
self.assertEqual('available', mock_snap.status)
self.assert_error_propagates(
[self.array.create_pgroup_snapshot],
self.driver.create_cgsnapshot, mock_context, mock_cgsnap)
@mock.patch(DRIVER_PATH + "._get_pgroup_snap_name", autospec=True)
def test_delete_cgsnapshot(self, mock_get_snap_name):
snap_name = "consisgroup-4a2f7e3a-312a-40c5-96a8-536b8a0f" \
"e074-cinder.4a2f7e3a-312a-40c5-96a8-536b8a0fe075"
mock_get_snap_name.return_value = snap_name
mock_cgsnap = mock.Mock()
mock_cgsnap.status = 'deleted'
mock_context = mock.Mock()
mock_snap = mock.MagicMock()
expected_snaps = [mock_snap]
self.driver.db = mock.Mock()
self.driver.db.snapshot_get_all_for_cgsnapshot.return_value = \
expected_snaps
model_update, snapshots = \
self.driver.delete_cgsnapshot(mock_context, mock_cgsnap)
self.array.delete_pgroup_snapshot.assert_called_with(snap_name)
self.assertEqual({'status': mock_cgsnap.status}, model_update)
self.assertEqual(expected_snaps, snapshots)
self.assertEqual('deleted', mock_snap.status)
self.array.delete_pgroup_snapshot.side_effect = \
exception.PureAPIException(
code=400,
reason="Protection group snapshot has been destroyed."
)
self.driver.delete_cgsnapshot(mock_context, mock_cgsnap)
self.array.delete_pgroup_snapshot.assert_called_with(snap_name)
self.array.delete_pgroup_snapshot.side_effect = \
exception.PureAPIException(
code=400,
reason="Protection group snapshot does not exist"
)
self.driver.delete_cgsnapshot(mock_context, mock_cgsnap)
self.array.delete_pgroup_snapshot.assert_called_with(snap_name)
self.array.delete_pgroup_snapshot.side_effect = \
exception.PureAPIException(
code=400,
reason="Some other error"
)
self.assertRaises(exception.PureAPIException,
self.driver.delete_cgsnapshot,
mock_context,
mock_cgsnap)
self.array.delete_pgroup_snapshot.side_effect = \
exception.PureAPIException(
code=500,
reason="Another different error"
)
self.assertRaises(exception.PureAPIException,
self.driver.delete_cgsnapshot,
mock_context,
mock_cgsnap)
self.array.delete_pgroup_snapshot.side_effect = None
self.assert_error_propagates(
[self.array.delete_pgroup_snapshot],
self.driver.delete_cgsnapshot, mock_context, mock_cgsnap)
class FlashArrayBaseTestCase(test.TestCase):

View File

@ -48,6 +48,9 @@ CONF.register_opts(PURE_OPTS)
INVALID_CHARACTERS = re.compile(r"[^-a-zA-Z0-9]")
GENERATED_NAME = re.compile(r".*-[a-f0-9]{32}-cinder$")
ERR_MSG_NOT_EXIST = "does not exist"
ERR_MSG_PENDING_ERADICATION = "has been destroyed"
def _get_vol_name(volume):
"""Return the name of the volume Purity will use."""
@ -59,6 +62,28 @@ def _get_snap_name(snapshot):
return "%s-cinder.%s" % (snapshot["volume_name"], snapshot["name"])
def _get_pgroup_name_from_id(id):
return "consisgroup-%s-cinder" % id
def _get_pgroup_snap_suffix(cgsnapshot):
return "cgsnapshot-%s-cinder" % cgsnapshot.id
def _get_pgroup_snap_name(cgsnapshot):
"""Return the name of the pgroup snapshot that Purity will use"""
return "%s.%s" % (_get_pgroup_name_from_id(cgsnapshot.consistencygroup_id),
_get_pgroup_snap_suffix(cgsnapshot))
def _get_pgroup_vol_snap_name(snapshot):
"""Return the name of the snapshot that Purity will use for a volume."""
cg_name = _get_pgroup_name_from_id(snapshot.cgsnapshot.consistencygroup_id)
cgsnapshot_id = _get_pgroup_snap_suffix(snapshot.cgsnapshot)
volume_name = snapshot.volume_name
return "%s.%s.%s-cinder" % (cg_name, cgsnapshot_id, volume_name)
def _generate_purity_host_name(name):
"""Return a valid Purity host name based on the name passed in."""
if len(name) > 23:
@ -71,7 +96,7 @@ def _generate_purity_host_name(name):
class PureISCSIDriver(san.SanISCSIDriver):
"""Performs volume management on Pure Storage FlashArray."""
VERSION = "2.0.0"
VERSION = "2.0.1"
def __init__(self, *args, **kwargs):
execute = kwargs.pop("execute", utils.execute)
@ -102,16 +127,31 @@ class PureISCSIDriver(san.SanISCSIDriver):
vol_name = _get_vol_name(volume)
vol_size = volume["size"] * units.Gi
self._array.create_volume(vol_name, vol_size)
if volume['consistencygroup_id']:
self._add_volume_to_consistency_group(
volume['consistencygroup_id'],
vol_name
)
LOG.debug("Leave PureISCSIDriver.create_volume.")
def create_volume_from_snapshot(self, volume, snapshot):
"""Creates a volume from a snapshot."""
LOG.debug("Enter PureISCSIDriver.create_volume_from_snapshot.")
vol_name = _get_vol_name(volume)
snap_name = _get_snap_name(snapshot)
if snapshot['cgsnapshot_id']:
snap_name = _get_pgroup_vol_snap_name(snapshot)
else:
snap_name = _get_snap_name(snapshot)
self._array.copy_volume(snap_name, vol_name)
self._extend_if_needed(vol_name, snapshot["volume_size"],
volume["size"])
if volume['consistencygroup_id']:
self._add_volume_to_consistency_group(
volume['consistencygroup_id'],
vol_name
)
LOG.debug("Leave PureISCSIDriver.create_volume_from_snapshot.")
def create_cloned_volume(self, volume, src_vref):
@ -121,6 +161,13 @@ class PureISCSIDriver(san.SanISCSIDriver):
src_name = _get_vol_name(src_vref)
self._array.copy_volume(src_name, vol_name)
self._extend_if_needed(vol_name, src_vref["size"], volume["size"])
if volume['consistencygroup_id']:
self._add_volume_to_consistency_group(
volume['consistencygroup_id'],
vol_name
)
LOG.debug("Leave PureISCSIDriver.create_cloned_volume.")
def _extend_if_needed(self, vol_name, src_size, vol_size):
@ -142,7 +189,7 @@ class PureISCSIDriver(san.SanISCSIDriver):
except exception.PureAPIException as err:
with excutils.save_and_reraise_exception() as ctxt:
if err.kwargs["code"] == 400 and \
"Volume does not exist" in err.msg:
ERR_MSG_NOT_EXIST in err.msg:
# Happens if the volume does not exist.
ctxt.reraise = False
LOG.warn(_LW("Volume deletion failed with message: %s")
@ -329,6 +376,7 @@ class PureISCSIDriver(san.SanISCSIDriver):
"total_capacity_gb": total,
"free_capacity_gb": free,
"reserved_percentage": 0,
"consistencygroup_support": True
}
self._stats = data
@ -340,6 +388,98 @@ class PureISCSIDriver(san.SanISCSIDriver):
self._array.extend_volume(vol_name, new_size)
LOG.debug("Leave PureISCSIDriver.extend_volume.")
def _add_volume_to_consistency_group(self, consistencygroup_id, vol_name):
pgroup_name = _get_pgroup_name_from_id(consistencygroup_id)
self._array.add_volume_to_pgroup(pgroup_name, vol_name)
def create_consistencygroup(self, context, group):
"""Creates a consistencygroup."""
LOG.debug("Enter PureISCSIDriver.create_consistencygroup")
self._array.create_pgroup(_get_pgroup_name_from_id(group.id))
model_update = {'status': 'available'}
LOG.debug("Leave PureISCSIDriver.create_consistencygroup")
return model_update
def delete_consistencygroup(self, context, group):
"""Deletes a consistency group."""
LOG.debug("Enter PureISCSIDriver.delete_consistencygroup")
try:
self._array.delete_pgroup(_get_pgroup_name_from_id(group.id))
except exception.PureAPIException as err:
with excutils.save_and_reraise_exception() as ctxt:
if (err.kwargs["code"] == 400 and
(ERR_MSG_PENDING_ERADICATION in err.msg or
ERR_MSG_NOT_EXIST in err.msg)):
# Treat these as a "success" case since we are trying
# to delete them anyway.
ctxt.reraise = False
LOG.warning(_LW("Unable to delete Protection Group: %s"),
err.msg)
volumes = self.db.volume_get_all_by_group(context, group.id)
for volume in volumes:
self.delete_volume(volume)
volume.status = 'deleted'
model_update = {'status': group['status']}
LOG.debug("Leave PureISCSIDriver.delete_consistencygroup")
return model_update, volumes
def create_cgsnapshot(self, context, cgsnapshot):
"""Creates a cgsnapshot."""
LOG.debug("Enter PureISCSIDriver.create_cgsnapshot")
pgroup_name = _get_pgroup_name_from_id(cgsnapshot.consistencygroup_id)
pgsnap_suffix = _get_pgroup_snap_suffix(cgsnapshot)
self._array.create_pgroup_snapshot(pgroup_name, pgsnap_suffix)
snapshots = self.db.snapshot_get_all_for_cgsnapshot(
context, cgsnapshot.id)
for snapshot in snapshots:
snapshot.status = 'available'
model_update = {'status': 'available'}
LOG.debug("Leave PureISCSIDriver.create_cgsnapshot")
return model_update, snapshots
def delete_cgsnapshot(self, context, cgsnapshot):
"""Deletes a cgsnapshot."""
LOG.debug("Enter PureISCSIDriver.delete_cgsnapshot")
pgsnap_name = _get_pgroup_snap_name(cgsnapshot)
try:
self._array.delete_pgroup_snapshot(pgsnap_name)
except exception.PureAPIException as err:
with excutils.save_and_reraise_exception() as ctxt:
if (err.kwargs["code"] == 400 and
(ERR_MSG_PENDING_ERADICATION in err.msg or
ERR_MSG_NOT_EXIST in err.msg)):
# Treat these as a "success" case since we are trying
# to delete them anyway.
ctxt.reraise = False
LOG.warning(_LW("Unable to delete Protection Group "
"Snapshot: %s"), err.msg)
snapshots = self.db.snapshot_get_all_for_cgsnapshot(
context, cgsnapshot.id)
for snapshot in snapshots:
snapshot.status = 'deleted'
model_update = {'status': cgsnapshot.status}
LOG.debug("Leave PureISCSIDriver.delete_cgsnapshot")
return model_update, snapshots
class FlashArray(object):
"""Wrapper for Pure Storage REST API."""