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
(cherry picked from commit 1f7f45e34a
)
Change-Id: I913570ac77951957f01b79b1851642ed99c38f51
This commit is contained in:
parent
b13dddac23
commit
0ea086e113
|
@ -17,10 +17,13 @@ import sys
|
|||
|
||||
import mock
|
||||
from oslo_config import cfg
|
||||
from oslo_utils import units
|
||||
|
||||
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
|
||||
|
||||
|
@ -29,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 = '2.0.3'
|
||||
|
||||
FAKE_ENUM_STRING = """
|
||||
<simpleType name="SmErrorType">
|
||||
|
@ -117,11 +122,29 @@ 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',
|
||||
'name': 'test_vol',
|
||||
'agent-type': 1,
|
||||
'size': int(1.75 * units.Gi),
|
||||
'online': True}}
|
||||
|
||||
FAKE_GET_VOL_INFO_ERROR = {
|
||||
|
@ -134,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,
|
||||
|
@ -475,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
|
||||
|
@ -492,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)
|
||||
@mock.patch.object(obj_volume.VolumeList, 'get_all',
|
||||
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=['test-backup-snap', 'volume-' + fake.volume_id]))
|
||||
def test_delete_volume_with_backup(self):
|
||||
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',
|
||||
|
@ -517,15 +581,18 @@ class NimbleDriverVolumeTestCase(NimbleDriverBaseTestCase):
|
|||
@mock.patch.object(obj_volume.VolumeList, 'get_all',
|
||||
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')
|
||||
@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 = \
|
||||
|
@ -534,25 +601,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,
|
||||
|
@ -561,7 +639,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)
|
||||
|
||||
|
@ -618,6 +696,21 @@ class NimbleDriverVolumeTestCase(NimbleDriverBaseTestCase):
|
|||
{'name': 'volume-abcdef'},
|
||||
{'source-name': 'test-vol'})
|
||||
|
||||
@mock.patch(NIMBLE_URLLIB2)
|
||||
@mock.patch(NIMBLE_CLIENT)
|
||||
@mock.patch.object(obj_volume.VolumeList, 'get_all',
|
||||
mock.Mock(return_value=[]))
|
||||
@NimbleDriverBaseTestCase.client_mock_decorator(create_configuration(
|
||||
'nimble', 'nimble_pass', '10.18.108.55', 'default', '*'))
|
||||
def test_manage_volume_get_size(self):
|
||||
self.mock_client_service.service.getNetConfig.return_value = (
|
||||
FAKE_POSITIVE_NETCONFIG_RESPONSE)
|
||||
self.mock_client_service.service.getVolInfo.return_value = (
|
||||
FAKE_GET_VOL_INFO_ONLINE)
|
||||
size = self.driver.manage_existing_get_size(
|
||||
{'name': 'volume-abcdef'}, {'source-name': 'test-vol'})
|
||||
self.assertEqual(1, size)
|
||||
|
||||
@mock.patch(NIMBLE_URLLIB2)
|
||||
@mock.patch(NIMBLE_CLIENT)
|
||||
@mock.patch.object(obj_volume.VolumeList, 'get_all',
|
||||
|
@ -726,7 +819,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',
|
||||
|
@ -739,6 +832,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',
|
||||
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):
|
||||
|
||||
|
@ -862,7 +982,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,
|
||||
|
@ -905,7 +1025,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,
|
||||
|
|
|
@ -22,6 +22,7 @@ import functools
|
|||
import random
|
||||
import re
|
||||
import six
|
||||
import ssl
|
||||
import string
|
||||
import sys
|
||||
|
||||
|
@ -38,7 +39,7 @@ from cinder.volume.drivers.san import san
|
|||
from cinder.volume import volume_types
|
||||
|
||||
|
||||
DRIVER_VERSION = '2.0.2'
|
||||
DRIVER_VERSION = '2.0.3'
|
||||
AES_256_XTS_CIPHER = 2
|
||||
DEFAULT_CIPHER = 3
|
||||
EXTRA_SPEC_ENCRYPTION = 'nimble:encryption'
|
||||
|
@ -61,6 +62,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 = [
|
||||
|
@ -97,6 +104,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
|
||||
2.0.3 - Support for Force Backup
|
||||
"""
|
||||
|
||||
VERSION = DRIVER_VERSION
|
||||
|
@ -211,14 +219,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
|
||||
|
@ -252,7 +303,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)
|
||||
|
@ -479,7 +530,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',
|
||||
|
@ -750,6 +801,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):
|
||||
|
@ -795,10 +871,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