Add support for force backup for Nimble Storage
Add capability to support backup of an in-use volume DocImpact Implements: blueprint nimble-add-force-backup Change-Id: I913570ac77951957f01b79b1851642ed99c38f51
This commit is contained in:
parent
76dd0d4003
commit
1f7f45e34a
@ -23,6 +23,7 @@ from cinder import context
|
||||
from cinder import exception
|
||||
from cinder.objects import volume as obj_volume
|
||||
from cinder import test
|
||||
from cinder.tests.unit import fake_constants as fake
|
||||
from cinder.volume.drivers import nimble
|
||||
from cinder.volume import volume_types
|
||||
|
||||
@ -31,6 +32,8 @@ CONF = cfg.CONF
|
||||
NIMBLE_CLIENT = 'cinder.volume.drivers.nimble.client'
|
||||
NIMBLE_URLLIB2 = 'six.moves.urllib.request'
|
||||
NIMBLE_RANDOM = 'cinder.volume.drivers.nimble.random'
|
||||
NIMBLE_ISCSI_DRIVER = 'cinder.volume.drivers.nimble.NimbleISCSIDriver'
|
||||
DRIVER_VERSION = '3.0.0'
|
||||
|
||||
FAKE_ENUM_STRING = """
|
||||
<simpleType name="SmErrorType">
|
||||
@ -119,6 +122,23 @@ FAKE_GET_VOL_INFO_RESPONSE = {
|
||||
'agent-type': 1,
|
||||
'online': False}}
|
||||
|
||||
FAKE_GET_VOL_INFO_BACKUP_RESPONSE = {
|
||||
'err-list': {'err-list': [{'code': 0}]},
|
||||
'vol': {'target-name': 'iqn.test',
|
||||
'name': 'test_vol',
|
||||
'agent-type': 1,
|
||||
'clone': 1,
|
||||
'base-snap': 'test-backup-snap',
|
||||
'parent-vol': 'volume-' + fake.VOLUME2_ID,
|
||||
'online': False}}
|
||||
|
||||
FAKE_GET_SNAP_INFO_BACKUP_RESPONSE = {
|
||||
'err-list': {'err-list': [{'code': 0}]},
|
||||
'snap': {'description': "backup-vol-" + fake.VOLUME2_ID,
|
||||
'name': 'test-backup-snap',
|
||||
'vol': 'volume-' + fake.VOLUME_ID}
|
||||
}
|
||||
|
||||
FAKE_GET_VOL_INFO_ONLINE = {
|
||||
'err-list': {'err-list': [{'code': 0}]},
|
||||
'vol': {'target-name': 'iqn.test',
|
||||
@ -137,8 +157,7 @@ FAKE_GET_VOL_INFO_RESPONSE_WITH_SET_AGENT_TYPE = {
|
||||
'name': 'test_vol',
|
||||
'agent-type': 5}}
|
||||
|
||||
|
||||
FAKE_TYPE_ID = 12345
|
||||
FAKE_TYPE_ID = fake.VOLUME_TYPE_ID
|
||||
|
||||
|
||||
def create_configuration(username, password, ip_address,
|
||||
@ -478,6 +497,8 @@ class NimbleDriverVolumeTestCase(NimbleDriverBaseTestCase):
|
||||
mock.Mock(return_value=[]))
|
||||
@NimbleDriverBaseTestCase.client_mock_decorator(create_configuration(
|
||||
'nimble', 'nimble_pass', '10.18.108.55', 'default', '*'))
|
||||
@mock.patch(NIMBLE_ISCSI_DRIVER + ".is_volume_backup_clone", mock.Mock(
|
||||
return_value = ['', '']))
|
||||
def test_delete_volume(self):
|
||||
self.mock_client_service.service.onlineVol.return_value = \
|
||||
FAKE_GENERIC_POSITIVE_RESPONSE
|
||||
@ -495,6 +516,46 @@ class NimbleDriverVolumeTestCase(NimbleDriverBaseTestCase):
|
||||
request={'name': 'testvolume', 'sid': 'a9b9aba7'})]
|
||||
self.mock_client_service.assert_has_calls(expected_calls)
|
||||
|
||||
@mock.patch(NIMBLE_URLLIB2)
|
||||
@mock.patch(NIMBLE_CLIENT)
|
||||
@NimbleDriverBaseTestCase.client_mock_decorator(create_configuration(
|
||||
'nimble', 'nimble_pass', '10.18.108.55', 'default', '*'))
|
||||
@mock.patch(NIMBLE_ISCSI_DRIVER + ".is_volume_backup_clone", mock.Mock(
|
||||
return_value=['test-backup-snap', 'volume-' + fake.VOLUME_ID]))
|
||||
@mock.patch.object(obj_volume.VolumeList, 'get_all_by_host')
|
||||
def test_delete_volume_with_backup(self, mock_volume_list):
|
||||
mock_volume_list.return_value = []
|
||||
self.mock_client_service.service.onlineVol.return_value = \
|
||||
FAKE_GENERIC_POSITIVE_RESPONSE
|
||||
self.mock_client_service.service.deleteVol.return_value = \
|
||||
FAKE_GENERIC_POSITIVE_RESPONSE
|
||||
self.mock_client_service.service.dissocProtPol.return_value = \
|
||||
FAKE_GENERIC_POSITIVE_RESPONSE
|
||||
self.mock_client_service.service.onlineSnap.return_value = \
|
||||
FAKE_GENERIC_POSITIVE_RESPONSE
|
||||
self.mock_client_service.service.deleteSnap.return_value = \
|
||||
FAKE_GENERIC_POSITIVE_RESPONSE
|
||||
|
||||
self.driver.delete_volume({'name': 'testvolume'})
|
||||
expected_calls = [mock.call.service.onlineVol(
|
||||
request={
|
||||
'online': False, 'name': 'testvolume', 'sid': 'a9b9aba7'}),
|
||||
mock.call.service.dissocProtPol(
|
||||
request={'vol-name': 'testvolume', 'sid': 'a9b9aba7'}),
|
||||
mock.call.service.deleteVol(
|
||||
request={'name': 'testvolume', 'sid': 'a9b9aba7'}),
|
||||
mock.call.service.onlineSnap(
|
||||
request={'vol': 'volume-' + fake.VOLUME_ID,
|
||||
'name': 'test-backup-snap',
|
||||
'online': False,
|
||||
'sid': 'a9b9aba7'}),
|
||||
mock.call.service.deleteSnap(
|
||||
request={'vol': 'volume-' + fake.VOLUME_ID,
|
||||
'name': 'test-backup-snap',
|
||||
'sid': 'a9b9aba7'})]
|
||||
|
||||
self.mock_client_service.assert_has_calls(expected_calls)
|
||||
|
||||
@mock.patch(NIMBLE_URLLIB2)
|
||||
@mock.patch(NIMBLE_CLIENT)
|
||||
@mock.patch.object(obj_volume.VolumeList, 'get_all_by_host',
|
||||
@ -517,18 +578,19 @@ class NimbleDriverVolumeTestCase(NimbleDriverBaseTestCase):
|
||||
|
||||
@mock.patch(NIMBLE_URLLIB2)
|
||||
@mock.patch(NIMBLE_CLIENT)
|
||||
@mock.patch.object(obj_volume.VolumeList, 'get_all_by_host',
|
||||
mock.Mock(return_value=[]))
|
||||
@mock.patch.object(volume_types, 'get_volume_type_extra_specs',
|
||||
mock.Mock(type_id=FAKE_TYPE_ID, return_value={
|
||||
'nimble:perfpol-name': 'default',
|
||||
mock.Mock(type_id=FAKE_TYPE_ID,
|
||||
return_value=
|
||||
{'nimble:perfpol-name': 'default',
|
||||
'nimble:encryption': 'yes',
|
||||
'nimble:multi-initiator': 'false'}))
|
||||
@NimbleDriverBaseTestCase.client_mock_decorator(create_configuration(
|
||||
'nimble', 'nimble_pass', '10.18.108.55', 'default', '*', False))
|
||||
@mock.patch.object(obj_volume.VolumeList, 'get_all_by_host')
|
||||
@mock.patch(NIMBLE_RANDOM)
|
||||
def test_create_cloned_volume(self, mock_random):
|
||||
mock_random.sample.return_value = 'abcdefghijkl'
|
||||
def test_create_cloned_volume(self, mock_random, mock_volume_list):
|
||||
mock_random.sample.return_value = fake.VOLUME_ID
|
||||
mock_volume_list.return_value = []
|
||||
self.mock_client_service.service.snapVol.return_value = \
|
||||
FAKE_GENERIC_POSITIVE_RESPONSE
|
||||
self.mock_client_service.service.cloneVol.return_value = \
|
||||
@ -537,25 +599,36 @@ class NimbleDriverVolumeTestCase(NimbleDriverBaseTestCase):
|
||||
FAKE_GET_VOL_INFO_RESPONSE
|
||||
self.mock_client_service.service.getNetConfig.return_value = \
|
||||
FAKE_POSITIVE_NETCONFIG_RESPONSE
|
||||
|
||||
volume = obj_volume.Volume(context.get_admin_context(),
|
||||
id=fake.VOLUME_ID,
|
||||
size=5.0,
|
||||
_name_id=None,
|
||||
display_name='',
|
||||
volume_type_id=FAKE_TYPE_ID
|
||||
)
|
||||
src_volume = obj_volume.Volume(context.get_admin_context(),
|
||||
id=fake.VOLUME2_ID,
|
||||
_name_id=None,
|
||||
size=5.0)
|
||||
self.assertEqual({
|
||||
'provider_location': '172.18.108.21:3260 iqn.test 0',
|
||||
'provider_auth': None},
|
||||
self.driver.create_cloned_volume({'name': 'volume',
|
||||
'size': 5,
|
||||
'volume_type_id': FAKE_TYPE_ID},
|
||||
{'name': 'testvolume',
|
||||
'size': 5}))
|
||||
self.driver.create_cloned_volume(volume, src_volume))
|
||||
expected_calls = [mock.call.service.snapVol(
|
||||
request={
|
||||
'vol': 'testvolume',
|
||||
'snapAttr': {'name': 'openstack-clone-volume-abcdefghijkl',
|
||||
'vol': "volume-" + fake.VOLUME2_ID,
|
||||
'snapAttr': {'name': 'openstack-clone-volume-' +
|
||||
fake.VOLUME_ID +
|
||||
"-" + fake.VOLUME_ID,
|
||||
'description': ''},
|
||||
'sid': 'a9b9aba7'}),
|
||||
mock.call.service.cloneVol(
|
||||
request={
|
||||
'snap-name': 'openstack-clone-volume-abcdefghijkl',
|
||||
'snap-name': 'openstack-clone-volume-' + fake.VOLUME_ID +
|
||||
"-" + fake.VOLUME_ID,
|
||||
'attr': {'snap-quota': sys.maxsize,
|
||||
'name': 'volume',
|
||||
'name': 'volume-' + fake.VOLUME_ID,
|
||||
'quota': 5368709120,
|
||||
'reserve': 5368709120,
|
||||
'online': True,
|
||||
@ -564,7 +637,7 @@ class NimbleDriverVolumeTestCase(NimbleDriverBaseTestCase):
|
||||
'multi-initiator': 'false',
|
||||
'perfpol-name': 'default',
|
||||
'agent-type': 5},
|
||||
'name': 'testvolume',
|
||||
'name': 'volume-' + fake.VOLUME2_ID,
|
||||
'sid': 'a9b9aba7'})]
|
||||
self.mock_client_service.assert_has_calls(expected_calls)
|
||||
|
||||
@ -744,7 +817,7 @@ class NimbleDriverVolumeTestCase(NimbleDriverBaseTestCase):
|
||||
def test_get_volume_stats(self):
|
||||
self.mock_client_service.service.getGroupConfig.return_value = \
|
||||
FAKE_POSITIVE_GROUP_CONFIG_RESPONSE
|
||||
expected_res = {'driver_version': '2.0.2',
|
||||
expected_res = {'driver_version': DRIVER_VERSION,
|
||||
'vendor_name': 'Nimble',
|
||||
'volume_backend_name': 'NIMBLE',
|
||||
'storage_protocol': 'iSCSI',
|
||||
@ -757,6 +830,33 @@ class NimbleDriverVolumeTestCase(NimbleDriverBaseTestCase):
|
||||
expected_res,
|
||||
self.driver.get_volume_stats(refresh=True))
|
||||
|
||||
@mock.patch(NIMBLE_URLLIB2)
|
||||
@mock.patch(NIMBLE_CLIENT)
|
||||
@mock.patch.object(obj_volume.VolumeList, 'get_all_by_host',
|
||||
mock.Mock(return_value=[]))
|
||||
@NimbleDriverBaseTestCase.client_mock_decorator(create_configuration(
|
||||
'nimble', 'nimble_pass', '10.18.108.55', 'default', '*'))
|
||||
def test_is_volume_backup_clone(self):
|
||||
self.mock_client_service.service.getVolInfo.return_value = \
|
||||
FAKE_GET_VOL_INFO_BACKUP_RESPONSE
|
||||
self.mock_client_service.service.getSnapInfo.return_value = \
|
||||
FAKE_GET_SNAP_INFO_BACKUP_RESPONSE
|
||||
volume = obj_volume.Volume(context.get_admin_context(),
|
||||
id=fake.VOLUME_ID,
|
||||
_name_id=None)
|
||||
self.assertEqual(("test-backup-snap", "volume-" + fake.VOLUME_ID),
|
||||
self.driver.is_volume_backup_clone(volume))
|
||||
expected_calls = [
|
||||
mock.call.service.getVolInfo(
|
||||
request={'name': 'volume-' + fake.VOLUME_ID,
|
||||
'sid': 'a9b9aba7'}),
|
||||
mock.call.service.getSnapInfo(
|
||||
request={'sid': 'a9b9aba7',
|
||||
'vol': 'volume-' + fake.VOLUME2_ID,
|
||||
'name': 'test-backup-snap'})
|
||||
]
|
||||
self.mock_client_service.assert_has_calls(expected_calls)
|
||||
|
||||
|
||||
class NimbleDriverSnapshotTestCase(NimbleDriverBaseTestCase):
|
||||
|
||||
@ -880,7 +980,7 @@ class NimbleDriverConnectionTestCase(NimbleDriverBaseTestCase):
|
||||
expected_res = {
|
||||
'driver_volume_type': 'iscsi',
|
||||
'data': {
|
||||
'target_lun': '14',
|
||||
'target_lun': 14,
|
||||
'volume_id': 12,
|
||||
'target_iqn': '13',
|
||||
'target_discovered': False,
|
||||
@ -923,7 +1023,7 @@ class NimbleDriverConnectionTestCase(NimbleDriverBaseTestCase):
|
||||
expected_res = {
|
||||
'driver_volume_type': 'iscsi',
|
||||
'data': {
|
||||
'target_lun': '14',
|
||||
'target_lun': 14,
|
||||
'volume_id': 12,
|
||||
'target_iqn': '13',
|
||||
'target_discovered': False,
|
||||
|
@ -23,6 +23,7 @@ import math
|
||||
import random
|
||||
import re
|
||||
import six
|
||||
import ssl
|
||||
import string
|
||||
import sys
|
||||
|
||||
@ -40,7 +41,7 @@ from cinder.volume.drivers.san import san
|
||||
from cinder.volume import volume_types
|
||||
|
||||
|
||||
DRIVER_VERSION = '2.0.2'
|
||||
DRIVER_VERSION = '3.0.0'
|
||||
AES_256_XTS_CIPHER = 2
|
||||
DEFAULT_CIPHER = 3
|
||||
EXTRA_SPEC_ENCRYPTION = 'nimble:encryption'
|
||||
@ -63,6 +64,12 @@ SM_SUBNET_MGMT_PLUS_DATA = 4
|
||||
LUN_ID = '0'
|
||||
WARN_LEVEL = 0.8
|
||||
|
||||
# Work around for ubuntu_openssl_bug_965371. Python soap client suds
|
||||
# throws the error ssl-certificate-verify-failed-error, workaround to disable
|
||||
# ssl check for now
|
||||
if hasattr(ssl, '_create_unverified_context'):
|
||||
ssl._create_default_https_context = ssl._create_unverified_context
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
nimble_opts = [
|
||||
@ -103,6 +110,7 @@ class NimbleISCSIDriver(san.SanISCSIDriver):
|
||||
Added Manage/Unmanage volume support
|
||||
2.0.1 - Added multi-initiator support through extra-specs
|
||||
2.0.2 - Fixed supporting extra specs while cloning vols
|
||||
3.0.0 - Newton Support for Force Backup
|
||||
"""
|
||||
|
||||
VERSION = DRIVER_VERSION
|
||||
@ -215,14 +223,57 @@ class NimbleISCSIDriver(san.SanISCSIDriver):
|
||||
self.configuration.nimble_pool_name, reserve)
|
||||
return self._get_model_info(volume['name'])
|
||||
|
||||
def is_volume_backup_clone(self, volume):
|
||||
"""Check if the volume is created through cinder-backup workflow.
|
||||
|
||||
:param volume: reference to volume from delete_volume()
|
||||
"""
|
||||
vol_info = self.APIExecutor.get_vol_info(volume.name)
|
||||
if vol_info['clone'] and vol_info['base-snap'] and vol_info[
|
||||
'parent-vol']:
|
||||
LOG.debug("Nimble base-snap exists for volume :%s", volume['name'])
|
||||
volume_name_prefix = volume.name.replace(volume.id, "")
|
||||
LOG.debug("volume_name_prefix : %s", volume_name_prefix)
|
||||
snap_info = self.APIExecutor.get_snap_info(vol_info['base-snap'],
|
||||
vol_info['parent-vol'])
|
||||
if snap_info['description'] and "backup-vol-" in snap_info[
|
||||
'description']:
|
||||
parent_vol_id = vol_info['parent-vol'
|
||||
].replace(volume_name_prefix, "")
|
||||
if "backup-vol-" + parent_vol_id in snap_info['description']:
|
||||
LOG.info(_LI("nimble backup-snapshot exists name: %s"),
|
||||
snap_info['name'])
|
||||
return snap_info['name'], snap_info['vol']
|
||||
return "", ""
|
||||
|
||||
def delete_volume(self, volume):
|
||||
"""Delete the specified volume."""
|
||||
snap_name, vol_name = self.is_volume_backup_clone(volume)
|
||||
self.APIExecutor.online_vol(volume['name'], False,
|
||||
ignore_list=['SM-enoent'])
|
||||
self.APIExecutor.dissociate_volcoll(volume['name'],
|
||||
ignore_list=['SM-enoent'])
|
||||
self.APIExecutor.delete_vol(volume['name'], ignore_list=['SM-enoent'])
|
||||
|
||||
# Nimble backend does not delete the snapshot from the parent volume
|
||||
# if there is a dependent clone. So the deletes need to be in reverse
|
||||
# order i.e.
|
||||
# 1. First delete the clone volume used for backup
|
||||
# 2. Delete the base snapshot used for clone from the parent volume.
|
||||
# This is only done for the force backup clone operation as it is
|
||||
# a temporary operation in which we are certain that the snapshot does
|
||||
# not need to be preserved after the backup is completed.
|
||||
|
||||
if snap_name and vol_name:
|
||||
self.APIExecutor.online_snap(vol_name,
|
||||
False,
|
||||
snap_name,
|
||||
ignore_list=['SM-ealready',
|
||||
'SM-enoent'])
|
||||
self.APIExecutor.delete_snap(vol_name,
|
||||
snap_name,
|
||||
ignore_list=['SM-enoent'])
|
||||
|
||||
def _generate_random_string(self, length):
|
||||
"""Generates random_string."""
|
||||
char_set = string.ascii_lowercase
|
||||
@ -256,7 +307,7 @@ class NimbleISCSIDriver(san.SanISCSIDriver):
|
||||
snapshot = {'volume_name': src_vref['name'],
|
||||
'name': snapshot_name,
|
||||
'volume_size': src_vref['size'],
|
||||
'display_name': '',
|
||||
'display_name': volume.display_name,
|
||||
'display_description': ''}
|
||||
self.APIExecutor.snap_vol(snapshot)
|
||||
self._clone_volume_from_snapshot(volume, snapshot)
|
||||
@ -483,7 +534,7 @@ class NimbleISCSIDriver(san.SanISCSIDriver):
|
||||
properties['target_discovered'] = False # whether discovery was used
|
||||
properties['target_portal'] = iscsi_portal
|
||||
properties['target_iqn'] = iqn
|
||||
properties['target_lun'] = lun_num
|
||||
properties['target_lun'] = int(lun_num)
|
||||
properties['volume_id'] = volume['id'] # used by xen currently
|
||||
return {
|
||||
'driver_volume_type': 'iscsi',
|
||||
@ -754,6 +805,31 @@ class NimbleAPIExecutor(object):
|
||||
vol_name)
|
||||
return response['vol']
|
||||
|
||||
@_connection_checker
|
||||
@_response_checker
|
||||
def _execute_get_snap_info(self, snap_name, vol_name):
|
||||
LOG.info(_LI('Getting snapshot information for %(vol_name)s '
|
||||
'%(snap_name)s'), {'vol_name': vol_name,
|
||||
'snap_name': snap_name})
|
||||
return self.client.service.getSnapInfo(request={'sid': self.sid,
|
||||
'vol': vol_name,
|
||||
'name': snap_name})
|
||||
|
||||
def get_snap_info(self, snap_name, vol_name):
|
||||
"""Get snapshot information.
|
||||
|
||||
:param snap_name: snapshot name
|
||||
:param vol_name: volume name
|
||||
:return: response object
|
||||
"""
|
||||
|
||||
response = self._execute_get_snap_info(snap_name, vol_name)
|
||||
LOG.info(_LI('Successfully got snapshot information for snapshot '
|
||||
'%(snap)s and %(volume)s'),
|
||||
{'snap': snap_name,
|
||||
'volume': vol_name})
|
||||
return response['snap']
|
||||
|
||||
@_connection_checker
|
||||
@_response_checker
|
||||
def online_vol(self, vol_name, online_flag, *args, **kwargs):
|
||||
@ -799,10 +875,12 @@ class NimbleAPIExecutor(object):
|
||||
volume_name = snapshot['volume_name']
|
||||
snap_name = snapshot['name']
|
||||
# Set snapshot description
|
||||
display_list = [getattr(snapshot, 'display_name', ''),
|
||||
display_list = [getattr(snapshot, 'display_name', snapshot[
|
||||
'display_name']),
|
||||
getattr(snapshot, 'display_description', '')]
|
||||
snap_description = ':'.join(filter(None, display_list))
|
||||
# Limit to 254 characters
|
||||
LOG.debug("snap_description %s", snap_description)
|
||||
snap_description = snap_description[:254]
|
||||
LOG.info(_LI('Creating snapshot for volume_name=%(vol)s'
|
||||
' snap_name=%(name)s snap_description=%(desc)s'),
|
||||
|
@ -0,0 +1,4 @@
|
||||
---
|
||||
features:
|
||||
- Support Force backup of in-use cinder volumes
|
||||
for Nimble Storage.
|
Loading…
Reference in New Issue
Block a user