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:
Raunak Kumar 2016-07-29 15:07:16 -07:00
parent b13dddac23
commit 0ea086e113
3 changed files with 227 additions and 25 deletions

View File

@ -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',
'nimble:encryption': 'yes',
'nimble:multi-initiator': 'false'}))
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,

View File

@ -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'),

View File

@ -0,0 +1,4 @@
---
features:
- Support Force backup of in-use cinder volumes
for Nimble Storage.