3PAR: Provide an option duing creation of volume from snapshot
A new extra spec has been added on the volume. While creating volume from snapshot, its value is checked and volume is created either as a snapshot/independent copy. Details of extra spec: Name - hpe3par:convert_to_base Value - True/False; defaults to False True: Volume (from snapshot) is created independently Advantage: new volumes are decoupled from initial snapshot and original volume Usage: where instances are launched for longer/unpredictable time frame False: Volume (from snapshot) is created as child of snapshot Advantage: new volumes can be created quickly Usage: where many similar instances are launched for short time frame Change-Id: Idda5512d8e35da401ee232524ec1d1fcd03bf9e2 Closes-Bug: #1814027
This commit is contained in:
parent
f715ebfa88
commit
f808499128
|
@ -254,6 +254,14 @@ class HPE3PARBaseDriver(test.TestCase):
|
|||
'volume_type': None,
|
||||
'volume_type_id': VOLUME_TYPE_ID_FLASH_CACHE}
|
||||
|
||||
volume_hos = {'name': VOLUME_NAME,
|
||||
'id': VOLUME_ID,
|
||||
'display_name': 'Foo Volume',
|
||||
'size': 2,
|
||||
'host': FAKE_CINDER_HOST,
|
||||
'volume_type': None,
|
||||
'volume_type_id': 'hos'}
|
||||
|
||||
snapshot = {'name': SNAPSHOT_NAME,
|
||||
'id': SNAPSHOT_ID,
|
||||
'user_id': USER_ID,
|
||||
|
@ -342,6 +350,13 @@ class HPE3PARBaseDriver(test.TestCase):
|
|||
'deleted_at': None,
|
||||
'id': VOLUME_TYPE_ID_FLASH_CACHE}
|
||||
|
||||
volume_type_hos = {'name': 'hos',
|
||||
'deleted': False,
|
||||
'updated_at': None,
|
||||
'extra_specs': {'convert_to_base': False},
|
||||
'deleted_at': None,
|
||||
'id': 'hos'}
|
||||
|
||||
flash_cache_3par_keys = {'flash_cache': 'true'}
|
||||
|
||||
cpgs = [
|
||||
|
@ -3215,31 +3230,22 @@ class TestHPE3PARDriverBase(HPE3PARBaseDriver):
|
|||
def test_delete_snapshot_in_use(self):
|
||||
# setup_mock_client drive with default configuration
|
||||
# and return the mock HTTP 3PAR client
|
||||
conf = {
|
||||
'getTask.return_value': {
|
||||
'status': 1},
|
||||
'copyVolume.return_value': {'taskid': 1},
|
||||
'getVolume.return_value': {}
|
||||
}
|
||||
mock_client = self.setup_driver(mock_conf=conf)
|
||||
mock_client = self.setup_driver()
|
||||
with mock.patch.object(hpecommon.HPE3PARCommon,
|
||||
'_create_client') as mock_create_client:
|
||||
mock_create_client.return_value = mock_client
|
||||
common = self.driver._login()
|
||||
self.driver._login()
|
||||
volume = self.volume.copy()
|
||||
model_update = self.driver.create_volume_from_snapshot(
|
||||
self.volume,
|
||||
volume,
|
||||
self.snapshot)
|
||||
self.assertIsNone(model_update)
|
||||
self.assertEqual(model_update, {})
|
||||
|
||||
comment = Comment({
|
||||
"snapshot_id": "2f823bdc-e36e-4dc8-bd15-de1c7a28ff31",
|
||||
"display_name": "Foo Volume",
|
||||
"volume_id": "d03338a9-9115-48a3-8dfc-35cdfcdc15a7",
|
||||
})
|
||||
volume_name_3par = common._encode_name(volume['id'])
|
||||
osv_matcher = 'osv-' + volume_name_3par
|
||||
omv_matcher = 'omv-' + volume_name_3par
|
||||
|
||||
expected = [
|
||||
mock.call.createSnapshot(
|
||||
|
@ -3247,13 +3253,7 @@ class TestHPE3PARDriverBase(HPE3PARBaseDriver):
|
|||
'oss-L4I73ONuTci9Fd4ceij-MQ',
|
||||
{
|
||||
'comment': comment,
|
||||
'readOnly': False}),
|
||||
mock.call.copyVolume(
|
||||
osv_matcher, omv_matcher, HPE3PAR_CPG, mock.ANY),
|
||||
mock.call.getTask(mock.ANY),
|
||||
mock.call.getVolume(osv_matcher),
|
||||
mock.call.deleteVolume(osv_matcher),
|
||||
mock.call.modifyVolume(omv_matcher, {'newName': osv_matcher})]
|
||||
'readOnly': False})]
|
||||
|
||||
mock_client.assert_has_calls(
|
||||
self.standard_login +
|
||||
|
@ -3290,31 +3290,22 @@ class TestHPE3PARDriverBase(HPE3PARBaseDriver):
|
|||
def test_create_volume_from_snapshot(self):
|
||||
# setup_mock_client drive with default configuration
|
||||
# and return the mock HTTP 3PAR client
|
||||
conf = {
|
||||
'getTask.return_value': {
|
||||
'status': 1},
|
||||
'copyVolume.return_value': {'taskid': 1},
|
||||
'getVolume.return_value': {}
|
||||
}
|
||||
mock_client = self.setup_driver(mock_conf=conf)
|
||||
mock_client = self.setup_driver()
|
||||
with mock.patch.object(hpecommon.HPE3PARCommon,
|
||||
'_create_client') as mock_create_client:
|
||||
mock_create_client.return_value = mock_client
|
||||
common = self.driver._login()
|
||||
self.driver._login()
|
||||
volume = self.volume.copy()
|
||||
model_update = self.driver.create_volume_from_snapshot(
|
||||
self.volume,
|
||||
volume,
|
||||
self.snapshot)
|
||||
self.assertIsNone(model_update)
|
||||
self.assertEqual(model_update, {})
|
||||
|
||||
comment = Comment({
|
||||
"snapshot_id": "2f823bdc-e36e-4dc8-bd15-de1c7a28ff31",
|
||||
"display_name": "Foo Volume",
|
||||
"volume_id": "d03338a9-9115-48a3-8dfc-35cdfcdc15a7",
|
||||
})
|
||||
volume_name_3par = common._encode_name(volume['id'])
|
||||
osv_matcher = 'osv-' + volume_name_3par
|
||||
omv_matcher = 'omv-' + volume_name_3par
|
||||
|
||||
expected = [
|
||||
mock.call.createSnapshot(
|
||||
|
@ -3322,13 +3313,7 @@ class TestHPE3PARDriverBase(HPE3PARBaseDriver):
|
|||
'oss-L4I73ONuTci9Fd4ceij-MQ',
|
||||
{
|
||||
'comment': comment,
|
||||
'readOnly': False}),
|
||||
mock.call.copyVolume(
|
||||
osv_matcher, omv_matcher, HPE3PAR_CPG, mock.ANY),
|
||||
mock.call.getTask(mock.ANY),
|
||||
mock.call.getVolume(osv_matcher),
|
||||
mock.call.deleteVolume(osv_matcher),
|
||||
mock.call.modifyVolume(omv_matcher, {'newName': osv_matcher})]
|
||||
'readOnly': False})]
|
||||
|
||||
mock_client.assert_has_calls(
|
||||
self.standard_login +
|
||||
|
@ -3481,13 +3466,7 @@ class TestHPE3PARDriverBase(HPE3PARBaseDriver):
|
|||
def test_create_volume_from_snapshot_qos(self, _mock_volume_types):
|
||||
# setup_mock_client drive with default configuration
|
||||
# and return the mock HTTP 3PAR client
|
||||
conf = {
|
||||
'getTask.return_value': {
|
||||
'status': 1},
|
||||
'copyVolume.return_value': {'taskid': 1},
|
||||
'getVolume.return_value': {}
|
||||
}
|
||||
mock_client = self.setup_driver(mock_conf=conf)
|
||||
mock_client = self.setup_driver()
|
||||
_mock_volume_types.return_value = {
|
||||
'name': 'gold',
|
||||
'extra_specs': {
|
||||
|
@ -3501,10 +3480,90 @@ class TestHPE3PARDriverBase(HPE3PARBaseDriver):
|
|||
with mock.patch.object(hpecommon.HPE3PARCommon,
|
||||
'_create_client') as mock_create_client:
|
||||
mock_create_client.return_value = mock_client
|
||||
common = self.driver._login()
|
||||
self.driver._login()
|
||||
volume = self.volume_qos.copy()
|
||||
model_update = self.driver.create_volume_from_snapshot(
|
||||
volume, self.snapshot)
|
||||
self.assertEqual(model_update, {})
|
||||
|
||||
comment = Comment({
|
||||
"snapshot_id": "2f823bdc-e36e-4dc8-bd15-de1c7a28ff31",
|
||||
"display_name": "Foo Volume",
|
||||
"volume_id": "d03338a9-9115-48a3-8dfc-35cdfcdc15a7",
|
||||
})
|
||||
|
||||
expected = [
|
||||
mock.call.createSnapshot(
|
||||
self.VOLUME_3PAR_NAME,
|
||||
'oss-L4I73ONuTci9Fd4ceij-MQ', {
|
||||
'comment': comment,
|
||||
'readOnly': False})]
|
||||
|
||||
mock_client.assert_has_calls(
|
||||
self.standard_login +
|
||||
expected +
|
||||
self.standard_logout)
|
||||
|
||||
@mock.patch.object(volume_types, 'get_volume_type')
|
||||
def test_create_volume_from_snapshot_as_child(self, _mock_volume_types):
|
||||
# setup_mock_client drive with default configuration
|
||||
# and return the mock HTTP 3PAR client
|
||||
mock_client = self.setup_driver()
|
||||
volume_type_hos = self.volume_type_hos
|
||||
volume_type_hos['extra_specs']['convert_to_base'] = False
|
||||
_mock_volume_types.return_value = volume_type_hos
|
||||
|
||||
with mock.patch.object(hpecommon.HPE3PARCommon,
|
||||
'_create_client') as mock_create_client:
|
||||
mock_create_client.return_value = mock_client
|
||||
self.driver._login()
|
||||
volume = self.volume_hos.copy()
|
||||
model_update = self.driver.create_volume_from_snapshot(
|
||||
volume,
|
||||
self.snapshot)
|
||||
self.assertEqual(model_update, {})
|
||||
|
||||
comment = Comment({
|
||||
"snapshot_id": "2f823bdc-e36e-4dc8-bd15-de1c7a28ff31",
|
||||
"display_name": "Foo Volume",
|
||||
"volume_id": "d03338a9-9115-48a3-8dfc-35cdfcdc15a7",
|
||||
})
|
||||
|
||||
expected = [
|
||||
mock.call.createSnapshot(
|
||||
self.VOLUME_3PAR_NAME,
|
||||
'oss-L4I73ONuTci9Fd4ceij-MQ',
|
||||
{
|
||||
'comment': comment,
|
||||
'readOnly': False})]
|
||||
|
||||
mock_client.assert_has_calls(
|
||||
self.standard_login +
|
||||
expected +
|
||||
self.standard_logout)
|
||||
|
||||
@mock.patch.object(volume_types, 'get_volume_type')
|
||||
def test_create_volume_from_snapshot_as_base(self, _mock_volume_types):
|
||||
# setup_mock_client drive with default configuration
|
||||
# and return the mock HTTP 3PAR client
|
||||
conf = {
|
||||
'getTask.return_value': {
|
||||
'status': 1},
|
||||
'copyVolume.return_value': {'taskid': 1},
|
||||
'getVolume.return_value': {}
|
||||
}
|
||||
mock_client = self.setup_driver(mock_conf=conf)
|
||||
volume_type_hos = self.volume_type_hos
|
||||
volume_type_hos['extra_specs']['convert_to_base'] = True
|
||||
_mock_volume_types.return_value = volume_type_hos
|
||||
with mock.patch.object(hpecommon.HPE3PARCommon,
|
||||
'_create_client') as mock_create_client:
|
||||
mock_create_client.return_value = mock_client
|
||||
common = self.driver._login()
|
||||
volume = self.volume_hos.copy()
|
||||
model_update = self.driver.create_volume_from_snapshot(
|
||||
volume,
|
||||
self.snapshot)
|
||||
self.assertIsNone(model_update)
|
||||
|
||||
comment = Comment({
|
||||
|
@ -3519,10 +3578,10 @@ class TestHPE3PARDriverBase(HPE3PARBaseDriver):
|
|||
expected = [
|
||||
mock.call.createSnapshot(
|
||||
self.VOLUME_3PAR_NAME,
|
||||
'oss-L4I73ONuTci9Fd4ceij-MQ', {
|
||||
'oss-L4I73ONuTci9Fd4ceij-MQ',
|
||||
{
|
||||
'comment': comment,
|
||||
'readOnly': False}),
|
||||
mock.call.getCPG(HPE3PAR_CPG),
|
||||
mock.call.copyVolume(
|
||||
osv_matcher, omv_matcher, HPE3PAR_CPG, mock.ANY),
|
||||
mock.call.getTask(mock.ANY),
|
||||
|
@ -3535,6 +3594,116 @@ class TestHPE3PARDriverBase(HPE3PARBaseDriver):
|
|||
expected +
|
||||
self.standard_logout)
|
||||
|
||||
@mock.patch.object(volume_types, 'get_volume_type')
|
||||
def test_create_volume_from_snapshot_as_child_and_extend(
|
||||
self, _mock_volume_types):
|
||||
# setup_mock_client drive with default configuration
|
||||
# and return the mock HTTP 3PAR client
|
||||
conf = {
|
||||
'getTask.return_value': {
|
||||
'status': 1},
|
||||
'copyVolume.return_value': {'taskid': 1},
|
||||
'getVolume.return_value': {}
|
||||
}
|
||||
mock_client = self.setup_driver(mock_conf=conf)
|
||||
volume_type_hos = self.volume_type_hos
|
||||
volume_type_hos['extra_specs']['convert_to_base'] = False
|
||||
_mock_volume_types.return_value = volume_type_hos
|
||||
with mock.patch.object(hpecommon.HPE3PARCommon,
|
||||
'_create_client') as mock_create_client:
|
||||
mock_create_client.return_value = mock_client
|
||||
common = self.driver._login()
|
||||
volume = self.volume_hos.copy()
|
||||
volume['size'] = self.volume['size'] + 10
|
||||
model_update = self.driver.create_volume_from_snapshot(
|
||||
volume,
|
||||
self.snapshot)
|
||||
self.assertIsNone(model_update)
|
||||
|
||||
comment = Comment({
|
||||
"snapshot_id": "2f823bdc-e36e-4dc8-bd15-de1c7a28ff31",
|
||||
"display_name": "Foo Volume",
|
||||
"volume_id": "d03338a9-9115-48a3-8dfc-35cdfcdc15a7",
|
||||
})
|
||||
volume_name_3par = common._encode_name(volume['id'])
|
||||
osv_matcher = 'osv-' + volume_name_3par
|
||||
omv_matcher = 'omv-' + volume_name_3par
|
||||
|
||||
expected = [
|
||||
mock.call.createSnapshot(
|
||||
self.VOLUME_3PAR_NAME,
|
||||
'oss-L4I73ONuTci9Fd4ceij-MQ',
|
||||
{
|
||||
'comment': comment,
|
||||
'readOnly': False}),
|
||||
mock.call.copyVolume(
|
||||
osv_matcher, omv_matcher, HPE3PAR_CPG, mock.ANY),
|
||||
mock.call.getTask(mock.ANY),
|
||||
mock.call.getVolume(osv_matcher),
|
||||
mock.call.deleteVolume(osv_matcher),
|
||||
mock.call.modifyVolume(omv_matcher, {'newName': osv_matcher}),
|
||||
mock.call.growVolume(osv_matcher, 10 * 1024)]
|
||||
|
||||
mock_client.assert_has_calls(
|
||||
self.standard_login +
|
||||
expected +
|
||||
self.standard_logout)
|
||||
|
||||
@mock.patch.object(volume_types, 'get_volume_type')
|
||||
def test_create_volume_from_snapshot_as_base_and_extend(
|
||||
self, _mock_volume_types):
|
||||
# setup_mock_client drive with default configuration
|
||||
# and return the mock HTTP 3PAR client
|
||||
conf = {
|
||||
'getTask.return_value': {
|
||||
'status': 1},
|
||||
'copyVolume.return_value': {'taskid': 1},
|
||||
'getVolume.return_value': {}
|
||||
}
|
||||
mock_client = self.setup_driver(mock_conf=conf)
|
||||
volume_type_hos = self.volume_type_hos
|
||||
volume_type_hos['extra_specs']['convert_to_base'] = True
|
||||
_mock_volume_types.return_value = volume_type_hos
|
||||
with mock.patch.object(hpecommon.HPE3PARCommon,
|
||||
'_create_client') as mock_create_client:
|
||||
mock_create_client.return_value = mock_client
|
||||
common = self.driver._login()
|
||||
volume = self.volume_hos.copy()
|
||||
volume['size'] = self.volume['size'] + 10
|
||||
model_update = self.driver.create_volume_from_snapshot(
|
||||
volume,
|
||||
self.snapshot)
|
||||
self.assertIsNone(model_update)
|
||||
|
||||
comment = Comment({
|
||||
"snapshot_id": "2f823bdc-e36e-4dc8-bd15-de1c7a28ff31",
|
||||
"display_name": "Foo Volume",
|
||||
"volume_id": "d03338a9-9115-48a3-8dfc-35cdfcdc15a7",
|
||||
})
|
||||
volume_name_3par = common._encode_name(volume['id'])
|
||||
osv_matcher = 'osv-' + volume_name_3par
|
||||
omv_matcher = 'omv-' + volume_name_3par
|
||||
|
||||
expected = [
|
||||
mock.call.createSnapshot(
|
||||
self.VOLUME_3PAR_NAME,
|
||||
'oss-L4I73ONuTci9Fd4ceij-MQ',
|
||||
{
|
||||
'comment': comment,
|
||||
'readOnly': False}),
|
||||
mock.call.copyVolume(
|
||||
osv_matcher, omv_matcher, HPE3PAR_CPG, mock.ANY),
|
||||
mock.call.getTask(mock.ANY),
|
||||
mock.call.getVolume(osv_matcher),
|
||||
mock.call.deleteVolume(osv_matcher),
|
||||
mock.call.modifyVolume(omv_matcher, {'newName': osv_matcher}),
|
||||
mock.call.growVolume(osv_matcher, 10 * 1024)]
|
||||
|
||||
mock_client.assert_has_calls(
|
||||
self.standard_login +
|
||||
expected +
|
||||
self.standard_logout)
|
||||
|
||||
def test_terminate_connection_from_primary_when_failed_over(self):
|
||||
# setup_mock_client drive with default configuration
|
||||
# and return the mock HTTP 3PAR client
|
||||
|
|
|
@ -273,11 +273,12 @@ class HPE3PARCommon(object):
|
|||
4.0.9 - Set proper backend on subsequent operation, after group
|
||||
failover. bug #1773069
|
||||
4.0.10 - Added retry in delete_volume. bug #1783934
|
||||
4.0.11 - Added extra spec hpe3par:convert_to_base
|
||||
|
||||
|
||||
"""
|
||||
|
||||
VERSION = "4.0.10"
|
||||
VERSION = "4.0.11"
|
||||
|
||||
stats = {}
|
||||
|
||||
|
@ -331,7 +332,8 @@ class HPE3PARCommon(object):
|
|||
'priority']
|
||||
qos_priority_level = {'low': 1, 'normal': 2, 'high': 3}
|
||||
hpe3par_valid_keys = ['cpg', 'snap_cpg', 'provisioning', 'persona', 'vvs',
|
||||
'flash_cache', 'compression', 'group_replication']
|
||||
'flash_cache', 'compression', 'group_replication',
|
||||
'convert_to_base']
|
||||
|
||||
def __init__(self, config, active_backend_id=None):
|
||||
self.config = config
|
||||
|
@ -1793,6 +1795,16 @@ class HPE3PARCommon(object):
|
|||
else:
|
||||
return default
|
||||
|
||||
def _get_boolean_key_value(self, hpe3par_keys, key, default=False):
|
||||
value = self._get_key_value(
|
||||
hpe3par_keys, key, default)
|
||||
if isinstance(value, six.string_types):
|
||||
if value.lower() == 'true':
|
||||
value = True
|
||||
else:
|
||||
value = False
|
||||
return value
|
||||
|
||||
def _get_qos_value(self, qos, key, default=None):
|
||||
if key in qos:
|
||||
return qos[key]
|
||||
|
@ -2082,6 +2094,10 @@ class HPE3PARCommon(object):
|
|||
hpe3par_tiramisu = (
|
||||
self._get_key_value(hpe3par_keys, 'group_replication'))
|
||||
|
||||
# by default, set convert_to_base to False
|
||||
convert_to_base = self._get_boolean_key_value(
|
||||
hpe3par_keys, 'convert_to_base', False)
|
||||
|
||||
# if provisioning is not set use thin
|
||||
default_prov = self.valid_prov_values[0]
|
||||
prov_value = self._get_key_value(hpe3par_keys, 'provisioning',
|
||||
|
@ -2117,7 +2133,8 @@ class HPE3PARCommon(object):
|
|||
'vvs_name': vvs_name, 'qos': qos,
|
||||
'tpvv': tpvv, 'tdvv': tdvv,
|
||||
'volume_type': volume_type,
|
||||
'group_replication': hpe3par_tiramisu}
|
||||
'group_replication': hpe3par_tiramisu,
|
||||
'convert_to_base': convert_to_base}
|
||||
|
||||
def get_volume_settings_from_type(self, volume, host=None):
|
||||
"""Get 3PAR volume settings given a volume.
|
||||
|
@ -2620,13 +2637,23 @@ class HPE3PARCommon(object):
|
|||
|
||||
self.client.createSnapshot(volume_name, snap_name, optional)
|
||||
|
||||
# Convert snapshot volume to base volume type
|
||||
LOG.debug('Converting to base volume type: %s.',
|
||||
volume['id'])
|
||||
model_update = self._convert_to_base_volume(volume)
|
||||
# by default, set convert_to_base to False
|
||||
convert_to_base = self._get_boolean_key_value(
|
||||
hpe3par_keys, 'convert_to_base', False)
|
||||
|
||||
LOG.debug("convert_to_base: %(convert)s",
|
||||
{'convert': convert_to_base})
|
||||
|
||||
# Grow the snapshot if needed
|
||||
growth_size = volume['size'] - snapshot['volume_size']
|
||||
LOG.debug("growth_size: %(size)s", {'size': growth_size})
|
||||
if growth_size > 0 or convert_to_base:
|
||||
# Convert snapshot volume to base volume type
|
||||
LOG.debug('Converting to base volume type: %(id)s.',
|
||||
{'id': volume['id']})
|
||||
model_update = self._convert_to_base_volume(volume)
|
||||
else:
|
||||
LOG.debug("volume is created as child of snapshot")
|
||||
|
||||
if growth_size > 0:
|
||||
try:
|
||||
growth_size_mib = growth_size * units.Gi / units.Mi
|
||||
|
@ -2922,6 +2949,37 @@ class HPE3PARCommon(object):
|
|||
"can't be deleted at this time.")
|
||||
raise exception.SnapshotIsBusy(message=msg)
|
||||
|
||||
if snap.startswith('osv-'):
|
||||
LOG.info(
|
||||
"Found a volume %(name)s",
|
||||
{'name': snap})
|
||||
|
||||
# Get details of original volume v1
|
||||
# These details would be required to form v2
|
||||
s1_detail = self.client.getVolume(snap_name)
|
||||
v1_name = s1_detail.get('copyOf')
|
||||
v1 = self.client.getVolume(v1_name)
|
||||
|
||||
# Get details of volume v2,
|
||||
# which is child of snapshot s1
|
||||
v2_name = snap
|
||||
v2 = self.client.getVolume(v2_name)
|
||||
|
||||
# Update v2 object as required for
|
||||
# _convert_to_base function
|
||||
v2['volume_type_id'] = \
|
||||
self._get_3par_vol_comment_value(
|
||||
v1['comment'], 'volume_type_id')
|
||||
|
||||
v2['id'] = self._get_3par_vol_comment_value(
|
||||
v2['comment'], 'volume_id')
|
||||
|
||||
v2['host'] = '#' + v1['userCPG']
|
||||
|
||||
LOG.debug('Converting to base volume type: '
|
||||
'%(id)s.', {'id': v2['id']})
|
||||
self._convert_to_base_volume(v2)
|
||||
|
||||
try:
|
||||
self.client.deleteVolume(snap_name)
|
||||
except Exception:
|
||||
|
|
Loading…
Reference in New Issue