"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,
"host": "irrelevant",
"volume_type": None,
"volume_type_id": None,
+ "consistencygroup_id": None
}
SNAPSHOT_ID = "04fe2f9a-d0c4-4564-a30d-693cc3657b47"
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"
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)
[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)
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"
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")
"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)
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):
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."""
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:
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)
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):
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):
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")
"total_capacity_gb": total,
"free_capacity_gb": free,
"reserved_percentage": 0,
+ "consistencygroup_support": True
}
self._stats = data
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."""