Adds the Violin Memory V7000 series FC driver.

This driver adds cinder volume support for VMEM 7300 and 7700 disk
arrays with fibrechannel HBAs.

DocImpact
Implements: blueprint vmem-7000-series-fc-driver
Change-Id: I516e12e699674fb0cdd9298f98d49bb14a2097ac
This commit is contained in:
Ryan Lucio 2015-06-09 13:17:53 -07:00
parent 4b1dff0e5c
commit 6e4cd718f8
5 changed files with 2598 additions and 0 deletions

View File

@ -21,8 +21,22 @@ import sys
import mock
# The following gymnastics to fake an exception class globally is done because
# we want to globally model and make available certain exceptions. If we do
# not do this, then the real-driver's import will not see our fakes.
class NoMatchingObjectIdError(Exception):
pass
error = mock.Mock()
error.NoMatchingObjectIdError = NoMatchingObjectIdError
core = mock.Mock()
core.attach_mock(error, 'error')
vmemclient = mock.Mock()
vmemclient.__version__ = "unknown"
vmemclient.attach_mock(core, 'core')
sys.modules['vmemclient'] = vmemclient
@ -42,4 +56,10 @@ mock_client_conf = [
'iscsi.create_iscsi_target',
'iscsi.delete_iscsi_target',
'igroup',
'client',
'client.get_client_info',
'client.create_client',
'client.delete_client',
'adapter',
'adapter.get_fc_info'
]

View File

@ -0,0 +1,777 @@
# Copyright 2015 Violin Memory, Inc.
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
"""
Tests for Violin Memory 7000 Series All-Flash Array Common Driver
"""
import math
import mock
from oslo_utils import units
from cinder import context
from cinder import exception
from cinder import test
from cinder.tests.unit import fake_vmem_client as vmemclient
from cinder.volume import configuration as conf
from cinder.volume.drivers.violin import v7000_common
from cinder.volume import volume_types
VOLUME_ID = "abcdabcd-1234-abcd-1234-abcdeffedcba"
VOLUME = {"name": "volume-" + VOLUME_ID,
"id": VOLUME_ID,
"display_name": "fake_volume",
"size": 2,
"host": "irrelevant",
"volume_type": None,
"volume_type_id": None,
}
SNAPSHOT_ID = "abcdabcd-1234-abcd-1234-abcdeffedcbb"
SNAPSHOT = {"name": "snapshot-" + SNAPSHOT_ID,
"id": SNAPSHOT_ID,
"volume_id": VOLUME_ID,
"volume_name": "volume-" + VOLUME_ID,
"volume_size": 2,
"display_name": "fake_snapshot",
}
SRC_VOL_ID = "abcdabcd-1234-abcd-1234-abcdeffedcbc"
SRC_VOL = {"name": "volume-" + SRC_VOL_ID,
"id": SRC_VOL_ID,
"display_name": "fake_src_vol",
"size": 2,
"host": "irrelevant",
"volume_type": None,
"volume_type_id": None,
}
INITIATOR_IQN = "iqn.1111-22.org.debian:11:222"
CONNECTOR = {"initiator": INITIATOR_IQN}
class V7000CommonTestCase(test.TestCase):
"""Test case for Violin drivers."""
def setUp(self):
super(V7000CommonTestCase, self).setUp()
self.conf = self.setup_configuration()
self.driver = v7000_common.V7000Common(self.conf)
self.driver.container = 'myContainer'
self.driver.device_id = 'ata-VIOLIN_MEMORY_ARRAY_23109R00000022'
self.stats = {}
def tearDown(self):
super(V7000CommonTestCase, self).tearDown()
def setup_configuration(self):
config = mock.Mock(spec=conf.Configuration)
config.volume_backend_name = 'v7000_common'
config.san_ip = '1.1.1.1'
config.san_login = 'admin'
config.san_password = ''
config.san_thin_provision = False
config.san_is_local = False
config.gateway_mga = '2.2.2.2'
config.gateway_mgb = '3.3.3.3'
config.use_igroups = False
config.violin_request_timeout = 300
config.container = 'myContainer'
return config
@mock.patch('vmemclient.open')
def setup_mock_client(self, _m_client, m_conf=None):
"""Create a fake backend communication factory.
The xg-tools creates a Concerto connection object (for V7000
devices) and returns it for use on a call to vmemclient.open().
"""
# configure the concerto object mock with defaults
_m_concerto = mock.Mock(name='Concerto',
version='1.1.1',
spec=vmemclient.mock_client_conf)
# if m_conf, clobber the defaults with it
if m_conf:
_m_concerto.configure_mock(**m_conf)
# set calls to vmemclient.open() to return this mocked concerto object
_m_client.return_value = _m_concerto
return _m_client
def setup_mock_concerto(self, m_conf=None):
"""Create a fake Concerto communication object."""
_m_concerto = mock.Mock(name='Concerto',
version='1.1.1',
spec=vmemclient.mock_client_conf)
if m_conf:
_m_concerto.configure_mock(**m_conf)
return _m_concerto
def test_check_for_setup_error(self):
"""No setup errors are found."""
self.driver.vmem_mg = self.setup_mock_concerto()
self.driver._is_supported_vmos_version = mock.Mock(return_value=True)
result = self.driver.check_for_setup_error()
self.driver._is_supported_vmos_version.assert_called_with(
self.driver.vmem_mg.version)
self.assertIsNone(result)
def test_create_lun(self):
"""Lun is successfully created."""
response = {'success': True, 'msg': 'Create resource successfully.'}
size_in_mb = VOLUME['size'] * units.Ki
conf = {
'lun.create_lun.return_value': response,
}
self.driver.vmem_mg = self.setup_mock_concerto(m_conf=conf)
self.driver._send_cmd = mock.Mock(return_value=response)
result = self.driver._create_lun(VOLUME)
self.driver._send_cmd.assert_called_with(
self.driver.vmem_mg.lun.create_lun,
'Create resource successfully.',
VOLUME['id'], size_in_mb, False, False, size_in_mb,
storage_pool=None)
self.assertIsNone(result)
def test_create_dedup_lun(self):
"""Lun is successfully created."""
vol = VOLUME.copy()
vol['size'] = 100
vol['volume_type_id'] = '1'
response = {'success': True, 'msg': 'Create resource successfully.'}
size_in_mb = vol['size'] * units.Ki
full_size_mb = size_in_mb
conf = {
'lun.create_lun.return_value': response,
}
self.driver.vmem_mg = self.setup_mock_concerto(m_conf=conf)
self.driver._send_cmd = mock.Mock(return_value=response)
# simulate extra specs of {'thin': 'true', 'dedupe': 'true'}
self.driver._get_volume_type_extra_spec = mock.Mock(
return_value="True")
self.driver._get_violin_extra_spec = mock.Mock(
return_value=None)
result = self.driver._create_lun(vol)
self.driver._send_cmd.assert_called_with(
self.driver.vmem_mg.lun.create_lun,
'Create resource successfully.',
VOLUME['id'], size_in_mb / 10, True, True, full_size_mb,
storage_pool=None)
self.assertIsNone(result)
def test_fail_extend_dedup_lun(self):
"""Volume extend fails when new size would shrink the volume."""
failure = exception.VolumeDriverException
vol = VOLUME.copy()
vol['volume_type_id'] = '1'
size_in_mb = vol['size'] * units.Ki
self.driver.vmem_mg = self.setup_mock_concerto()
# simulate extra specs of {'thin': 'true', 'dedupe': 'true'}
self.driver._get_volume_type_extra_spec = mock.Mock(
return_value="True")
self.assertRaises(failure, self.driver._extend_lun,
vol, size_in_mb)
def test_create_non_dedup_lun(self):
"""Lun is successfully created."""
vol = VOLUME.copy()
vol['size'] = 100
vol['volume_type_id'] = '1'
response = {'success': True, 'msg': 'Create resource successfully.'}
size_in_mb = vol['size'] * units.Ki
full_size_mb = size_in_mb
conf = {
'lun.create_lun.return_value': response,
}
self.driver.vmem_mg = self.setup_mock_concerto(m_conf=conf)
self.driver._send_cmd = mock.Mock(return_value=response)
# simulate extra specs of {'thin': 'false', 'dedupe': 'false'}
self.driver._get_volume_type_extra_spec = mock.Mock(
return_value="False")
self.driver._get_violin_extra_spec = mock.Mock(
return_value=None)
result = self.driver._create_lun(vol)
self.driver._send_cmd.assert_called_with(
self.driver.vmem_mg.lun.create_lun,
'Create resource successfully.',
VOLUME['id'], size_in_mb, False, False, full_size_mb,
storage_pool=None)
self.assertIsNone(result)
def test_create_lun_fails(self):
"""Array returns error that the lun already exists."""
response = {'success': False,
'msg': 'Duplicate Virtual Device name. Error: 0x90010022'}
conf = {
'lun.create_lun.return_value': response,
}
self.driver.vmem_mg = self.setup_mock_concerto(m_conf=conf)
self.driver._send_cmd = mock.Mock(return_value=response)
self.assertIsNone(self.driver._create_lun(VOLUME))
def test_create_lun_on_a_storage_pool(self):
"""Lun is successfully created."""
vol = VOLUME.copy()
vol['size'] = 100
vol['volume_type_id'] = '1'
response = {'success': True, 'msg': 'Create resource successfully.'}
size_in_mb = vol['size'] * units.Ki
full_size_mb = size_in_mb
conf = {
'lun.create_lun.return_value': response,
}
self.driver.vmem_mg = self.setup_mock_concerto(m_conf=conf)
self.driver._send_cmd = mock.Mock(return_value=response)
self.driver._get_volume_type_extra_spec = mock.Mock(
return_value="False")
# simulates extra specs: {'storage_pool', 'StoragePool'}
self.driver._get_violin_extra_spec = mock.Mock(
return_value="StoragePool")
result = self.driver._create_lun(vol)
self.driver._send_cmd.assert_called_with(
self.driver.vmem_mg.lun.create_lun,
'Create resource successfully.',
VOLUME['id'], size_in_mb, False, False, full_size_mb,
storage_pool="StoragePool")
self.assertIsNone(result)
def test_delete_lun(self):
"""Lun is deleted successfully."""
response = {'success': True, 'msg': 'Delete resource successfully'}
success_msgs = ['Delete resource successfully', '']
conf = {
'lun.delete_lun.return_value': response,
}
self.driver.vmem_mg = self.setup_mock_concerto(m_conf=conf)
self.driver._send_cmd = mock.Mock(return_value=response)
self.driver._delete_lun_snapshot_bookkeeping = mock.Mock()
result = self.driver._delete_lun(VOLUME)
self.driver._send_cmd.assert_called_with(
self.driver.vmem_mg.lun.delete_lun,
success_msgs, VOLUME['id'], True)
self.driver._delete_lun_snapshot_bookkeeping.assert_called_with(
VOLUME['id'])
self.assertIsNone(result)
# TODO(rlucio) More delete lun failure cases to be added after
# collecting the possible responses from Concerto
def test_extend_lun(self):
"""Volume extend completes successfully."""
new_volume_size = 10
change_in_size_mb = (new_volume_size - VOLUME['size']) * units.Ki
response = {'success': True, 'message': 'Expand resource successfully'}
conf = {
'lun.extend_lun.return_value': response,
}
self.driver.vmem_mg = self.setup_mock_concerto(m_conf=conf)
self.driver._send_cmd = mock.Mock(return_value=response)
result = self.driver._extend_lun(VOLUME, new_volume_size)
self.driver._send_cmd.assert_called_with(
self.driver.vmem_mg.lun.extend_lun,
response['message'], VOLUME['id'], change_in_size_mb)
self.assertIsNone(result)
def test_extend_lun_new_size_is_too_small(self):
"""Volume extend fails when new size would shrink the volume."""
new_volume_size = 0
change_in_size_mb = (new_volume_size - VOLUME['size']) * units.Ki
response = {'success': False, 'msg': 'Invalid size. Error: 0x0902000c'}
failure = exception.ViolinBackendErr
conf = {
'lun.resize_lun.return_value': response,
}
self.driver.vmem_mg = self.setup_mock_concerto(m_conf=conf)
self.driver._send_cmd = mock.Mock(side_effect=failure(message='fail'))
self.assertRaises(failure, self.driver._extend_lun,
VOLUME, change_in_size_mb)
def test_create_volume_from_snapshot(self):
"""Create a new cinder volume from a given snapshot of a lun."""
object_id = '12345'
vdev_id = 11111
response = {'success': True,
'object_id': object_id,
'msg': 'Copy TimeMark successfully.'}
lun_info = {'virtualDeviceID': vdev_id}
compressed_snap_id = 'abcdabcd1234abcd1234abcdeffedcbb'
conf = {
'lun.copy_snapshot_to_new_lun.return_value': response,
}
self.driver.vmem_mg = self.setup_mock_concerto(m_conf=conf)
self.driver._compress_snapshot_id = mock.Mock(
return_value=compressed_snap_id)
self.driver.vmem_mg.lun.get_lun_info = mock.Mock(return_value=lun_info)
self.driver._wait_for_lun_or_snap_copy = mock.Mock()
result = self.driver._create_volume_from_snapshot(SNAPSHOT, VOLUME)
self.driver.vmem_mg.lun.copy_snapshot_to_new_lun.assert_called_with(
source_lun=SNAPSHOT['volume_id'],
source_snapshot_comment=compressed_snap_id,
destination=VOLUME['id'], storage_pool=None)
self.driver.vmem_mg.lun.get_lun_info.assert_called_with(
object_id=object_id)
self.driver._wait_for_lun_or_snap_copy.assert_called_with(
SNAPSHOT['volume_id'], dest_vdev_id=vdev_id)
self.assertIsNone(result)
def test_create_volume_from_snapshot_on_a_storage_pool(self):
"""Create a new cinder volume from a given snapshot of a lun."""
dest_vol = VOLUME.copy()
dest_vol['size'] = 100
dest_vol['volume_type_id'] = '1'
object_id = '12345'
vdev_id = 11111
response = {'success': True,
'object_id': object_id,
'msg': 'Copy TimeMark successfully.'}
lun_info = {'virtualDeviceID': vdev_id}
compressed_snap_id = 'abcdabcd1234abcd1234abcdeffedcbb'
conf = {
'lun.copy_snapshot_to_new_lun.return_value': response,
}
self.driver.vmem_mg = self.setup_mock_concerto(m_conf=conf)
self.driver._compress_snapshot_id = mock.Mock(
return_value=compressed_snap_id)
self.driver.vmem_mg.lun.get_lun_info = mock.Mock(return_value=lun_info)
self.driver._wait_for_lun_or_snap_copy = mock.Mock()
# simulates extra specs: {'storage_pool', 'StoragePool'}
self.driver._get_violin_extra_spec = mock.Mock(
return_value="StoragePool")
result = self.driver._create_volume_from_snapshot(SNAPSHOT, dest_vol)
self.assertIsNone(result)
def test_create_volume_from_snapshot_fails(self):
"""Array returns error that the lun already exists."""
response = {'success': False,
'msg': 'Duplicate Virtual Device name. Error: 0x90010022'}
compressed_snap_id = 'abcdabcd1234abcd1234abcdeffedcbb'
failure = exception.ViolinBackendErrExists
conf = {
'lun.copy_snapshot_to_new_lun.return_value': response,
}
self.driver.vmem_mg = self.setup_mock_concerto(m_conf=conf)
self.driver._send_cmd = mock.Mock(return_value=response)
self.driver._compress_snapshot_id = mock.Mock(
return_value=compressed_snap_id)
self.driver._send_cmd = mock.Mock(side_effect=failure(message='fail'))
self.assertRaises(failure, self.driver._create_volume_from_snapshot,
SNAPSHOT, VOLUME)
def test_create_lun_from_lun(self):
"""lun full clone to new volume completes successfully."""
object_id = '12345'
response = {'success': True,
'object_id': object_id,
'msg': 'Copy Snapshot resource successfully'}
conf = {
'lun.copy_lun_to_new_lun.return_value': response,
}
self.driver.vmem_mg = self.setup_mock_concerto(m_conf=conf)
self.driver._ensure_snapshot_resource_area = mock.Mock()
self.driver._wait_for_lun_or_snap_copy = mock.Mock()
result = self.driver._create_lun_from_lun(SRC_VOL, VOLUME)
self.driver._ensure_snapshot_resource_area.assert_called_with(
SRC_VOL['id'])
self.driver.vmem_mg.lun.copy_lun_to_new_lun.assert_called_with(
source=SRC_VOL['id'], destination=VOLUME['id'], storage_pool=None)
self.driver._wait_for_lun_or_snap_copy.assert_called_with(
SRC_VOL['id'], dest_obj_id=object_id)
self.assertIsNone(result)
def test_create_lun_from_lun_on_a_storage_pool(self):
"""lun full clone to new volume completes successfully."""
dest_vol = VOLUME.copy()
dest_vol['size'] = 100
dest_vol['volume_type_id'] = '1'
object_id = '12345'
response = {'success': True,
'object_id': object_id,
'msg': 'Copy Snapshot resource successfully'}
conf = {
'lun.copy_lun_to_new_lun.return_value': response,
}
self.driver.vmem_mg = self.setup_mock_concerto(m_conf=conf)
self.driver._ensure_snapshot_resource_area = mock.Mock()
self.driver._wait_for_lun_or_snap_copy = mock.Mock()
# simulates extra specs: {'storage_pool', 'StoragePool'}
self.driver._get_violin_extra_spec = mock.Mock(
return_value="StoragePool")
result = self.driver._create_lun_from_lun(SRC_VOL, dest_vol)
self.driver._ensure_snapshot_resource_area.assert_called_with(
SRC_VOL['id'])
self.driver.vmem_mg.lun.copy_lun_to_new_lun.assert_called_with(
source=SRC_VOL['id'], destination=dest_vol['id'],
storage_pool="StoragePool")
self.driver._wait_for_lun_or_snap_copy.assert_called_with(
SRC_VOL['id'], dest_obj_id=object_id)
self.assertIsNone(result)
def test_create_lun_from_lun_fails(self):
"""lun full clone to new volume completes successfully."""
failure = exception.ViolinBackendErr
response = {'success': False,
'msg': 'Snapshot Resource is not created '
'for this virtual device. Error: 0x0901008c'}
conf = {
'lun.copy_lun_to_new_lun.return_value': response,
}
self.driver.vmem_mg = self.setup_mock_concerto(m_conf=conf)
self.driver._ensure_snapshot_resource_area = mock.Mock()
self.driver._send_cmd = mock.Mock(side_effect=failure(message='fail'))
self.assertRaises(failure, self.driver._create_lun_from_lun,
SRC_VOL, VOLUME)
def test_send_cmd(self):
"""Command callback completes successfully."""
success_msg = 'success'
request_args = ['arg1', 'arg2', 'arg3']
response = {'success': True, 'msg': 'Operation successful'}
request_func = mock.Mock(return_value=response)
result = self.driver._send_cmd(request_func, success_msg, request_args)
self.assertEqual(response, result)
def test_send_cmd_request_timed_out(self):
"""The callback retry timeout hits immediately."""
failure = exception.ViolinRequestRetryTimeout
success_msg = 'success'
request_args = ['arg1', 'arg2', 'arg3']
self.conf.violin_request_timeout = 0
request_func = mock.Mock()
self.assertRaises(failure, self.driver._send_cmd,
request_func, success_msg, request_args)
def test_send_cmd_response_has_no_message(self):
"""The callback returns no message on the first call."""
success_msg = 'success'
request_args = ['arg1', 'arg2', 'arg3']
response1 = {'success': True, 'msg': None}
response2 = {'success': True, 'msg': 'success'}
request_func = mock.Mock(side_effect=[response1, response2])
self.assertEqual(response2, self.driver._send_cmd
(request_func, success_msg, request_args))
def test_check_error_code(self):
"""Return an exception for a valid error code."""
failure = exception.ViolinBackendErr
response = {'success': False, 'msg': 'Error: 0x90000000'}
self.assertRaises(failure, self.driver._check_error_code,
response)
def test_check_error_code_non_fatal_error(self):
"""Returns no exception for a non-fatal error code."""
response = {'success': False, 'msg': 'Error: 0x9001003c'}
self.assertIsNone(self.driver._check_error_code(response))
def test_compress_snapshot_id(self):
test_snap_id = "12345678-abcd-1234-cdef-0123456789ab"
expected = "12345678abcd1234cdef0123456789ab"
self.assertTrue(len(expected) == 32)
result = self.driver._compress_snapshot_id(test_snap_id)
self.assertTrue(result == expected)
def test_ensure_snapshot_resource_area(self):
result_dict = {'success': True, 'res': 'Successful'}
self.driver.vmem_mg = self.setup_mock_concerto()
snap = self.driver.vmem_mg.snapshot
snap.lun_has_a_snapshot_resource = mock.Mock(return_value=False)
snap.create_snapshot_resource = mock.Mock(return_value=result_dict)
with mock.patch('cinder.db.sqlalchemy.api.volume_get',
return_value=VOLUME):
result = self.driver._ensure_snapshot_resource_area(VOLUME_ID)
self.assertIsNone(result)
snap.lun_has_a_snapshot_resource.assert_called_with(lun=VOLUME_ID)
snap.create_snapshot_resource.assert_called_with(
lun=VOLUME_ID,
size=int(math.ceil(0.2 * (VOLUME['size'] * 1024))),
enable_notification=False,
policy=v7000_common.CONCERTO_DEFAULT_SRA_POLICY,
enable_expansion=
v7000_common.CONCERTO_DEFAULT_SRA_ENABLE_EXPANSION,
expansion_threshold=
v7000_common.CONCERTO_DEFAULT_SRA_EXPANSION_THRESHOLD,
expansion_increment=
v7000_common.CONCERTO_DEFAULT_SRA_EXPANSION_INCREMENT,
expansion_max_size=
v7000_common.CONCERTO_DEFAULT_SRA_EXPANSION_MAX_SIZE,
enable_shrink=v7000_common.CONCERTO_DEFAULT_SRA_ENABLE_SHRINK,
storage_pool=None)
def test_ensure_snapshot_resource_area_with_storage_pool(self):
dest_vol = VOLUME.copy()
dest_vol['size'] = 2
dest_vol['volume_type_id'] = '1'
result_dict = {'success': True, 'res': 'Successful'}
self.driver.vmem_mg = self.setup_mock_concerto()
snap = self.driver.vmem_mg.snapshot
snap.lun_has_a_snapshot_resource = mock.Mock(return_value=False)
snap.create_snapshot_resource = mock.Mock(return_value=result_dict)
# simulates extra specs: {'storage_pool', 'StoragePool'}
self.driver._get_violin_extra_spec = mock.Mock(
return_value="StoragePool")
with mock.patch('cinder.db.sqlalchemy.api.volume_get',
return_value=dest_vol):
result = self.driver._ensure_snapshot_resource_area(VOLUME_ID)
self.assertIsNone(result)
snap.lun_has_a_snapshot_resource.assert_called_with(lun=VOLUME_ID)
snap.create_snapshot_resource.assert_called_with(
lun=VOLUME_ID,
size=int(math.ceil(0.2 * (VOLUME['size'] * 1024))),
enable_notification=False,
policy=v7000_common.CONCERTO_DEFAULT_SRA_POLICY,
enable_expansion=
v7000_common.CONCERTO_DEFAULT_SRA_ENABLE_EXPANSION,
expansion_threshold=
v7000_common.CONCERTO_DEFAULT_SRA_EXPANSION_THRESHOLD,
expansion_increment=
v7000_common.CONCERTO_DEFAULT_SRA_EXPANSION_INCREMENT,
expansion_max_size=
v7000_common.CONCERTO_DEFAULT_SRA_EXPANSION_MAX_SIZE,
enable_shrink=v7000_common.CONCERTO_DEFAULT_SRA_ENABLE_SHRINK,
storage_pool="StoragePool")
def test_ensure_snapshot_resource_policy(self):
result_dict = {'success': True, 'res': 'Successful'}
self.driver.vmem_mg = self.setup_mock_concerto()
snap = self.driver.vmem_mg.snapshot
snap.lun_has_a_snapshot_policy = mock.Mock(return_value=False)
snap.create_snapshot_policy = mock.Mock(return_value=result_dict)
result = self.driver._ensure_snapshot_policy(VOLUME_ID)
self.assertIsNone(result)
snap.lun_has_a_snapshot_policy.assert_called_with(lun=VOLUME_ID)
snap.create_snapshot_policy.assert_called_with(
lun=VOLUME_ID,
max_snapshots=v7000_common.CONCERTO_DEFAULT_POLICY_MAX_SNAPSHOTS,
enable_replication=False,
enable_snapshot_schedule=False,
enable_cdp=False,
retention_mode=v7000_common.CONCERTO_DEFAULT_POLICY_RETENTION_MODE)
def test_delete_lun_snapshot_bookkeeping(self):
result_dict = {'success': True, 'res': 'Successful'}
self.driver.vmem_mg = self.setup_mock_concerto()
snap = self.driver.vmem_mg.snapshot
snap.get_snapshots = mock.Mock(
return_value=[],
side_effect=vmemclient.core.error.NoMatchingObjectIdError)
snap.delete_snapshot_policy = mock.Mock(return_value=result_dict)
snap.delete_snapshot_resource = mock.Mock()
result = self.driver._delete_lun_snapshot_bookkeeping(
volume_id=VOLUME_ID)
self.assertIsNone(result)
snap.get_snapshots.assert_called_with(VOLUME_ID)
snap.delete_snapshot_policy.assert_called_with(lun=VOLUME_ID)
snap.delete_snapshot_resource.assert_called_with(lun=VOLUME_ID)
def test_create_lun_snapshot(self):
response = {'success': True, 'msg': 'Create TimeMark successfully'}
self.driver.vmem_mg = self.setup_mock_concerto()
self.driver._ensure_snapshot_resource_area = (
mock.Mock(return_value=True))
self.driver._ensure_snapshot_policy = mock.Mock(return_value=True)
self.driver._send_cmd = mock.Mock(return_value=response)
with mock.patch('cinder.db.sqlalchemy.api.volume_get',
return_value=VOLUME):
result = self.driver._create_lun_snapshot(SNAPSHOT)
self.assertIsNone(result)
self.driver._ensure_snapshot_resource_area.assert_called_with(
VOLUME_ID)
self.driver._ensure_snapshot_policy.assert_called_with(VOLUME_ID)
self.driver._send_cmd.assert_called_with(
self.driver.vmem_mg.snapshot.create_lun_snapshot,
'Create TimeMark successfully',
lun=VOLUME_ID,
comment=self.driver._compress_snapshot_id(SNAPSHOT_ID),
priority=v7000_common.CONCERTO_DEFAULT_PRIORITY,
enable_notification=False)
def test_delete_lun_snapshot(self):
response = {'success': True, 'msg': 'Delete TimeMark successfully'}
compressed_snap_id = 'abcdabcd1234abcd1234abcdeffedcbb'
self.driver.vmem_mg = self.setup_mock_concerto()
self.driver._send_cmd = mock.Mock(return_value=response)
self.driver._compress_snapshot_id = mock.Mock(
return_value=compressed_snap_id)
self.assertIsNone(self.driver._delete_lun_snapshot(SNAPSHOT))
self.driver._send_cmd.assert_called_with(
self.driver.vmem_mg.snapshot.delete_lun_snapshot,
'Delete TimeMark successfully',
lun=VOLUME_ID,
comment=compressed_snap_id)
def test_wait_for_lun_or_snap_copy_completes_for_snap(self):
"""waiting for a snapshot to copy succeeds."""
vdev_id = 11111
response = (vdev_id, None, 100)
conf = {
'snapshot.get_snapshot_copy_status.return_value': response,
}
self.driver.vmem_mg = self.setup_mock_concerto(m_conf=conf)
result = self.driver._wait_for_lun_or_snap_copy(
SRC_VOL['id'], dest_vdev_id=vdev_id)
(self.driver.vmem_mg.snapshot.get_snapshot_copy_status.
assert_called_with(SRC_VOL['id']))
self.assertTrue(result)
def test_wait_for_lun_or_snap_copy_completes_for_lun(self):
"""waiting for a lun to copy succeeds."""
object_id = '12345'
response = (object_id, None, 100)
conf = {
'lun.get_lun_copy_status.return_value': response,
}
self.driver.vmem_mg = self.setup_mock_concerto(m_conf=conf)
result = self.driver._wait_for_lun_or_snap_copy(
SRC_VOL['id'], dest_obj_id=object_id)
self.driver.vmem_mg.lun.get_lun_copy_status.assert_called_with(
SRC_VOL['id'])
self.assertTrue(result)
@mock.patch.object(context, 'get_admin_context')
@mock.patch.object(volume_types, 'get_volume_type')
def test_get_volume_type_extra_spec(self,
m_get_volume_type,
m_get_admin_context):
'''Volume_type extra specs are found successfully.'''
vol = VOLUME.copy()
vol['volume_type_id'] = 1
volume_type = {'extra_specs': {'override:test_key': 'test_value'}}
m_get_admin_context.return_value = None
m_get_volume_type.return_value = volume_type
result = self.driver._get_volume_type_extra_spec(vol, 'test_key')
m_get_admin_context.assert_called_with()
m_get_volume_type.assert_called_with(None, vol['volume_type_id'])
self.assertEqual('test_value', result)
@mock.patch.object(context, 'get_admin_context')
@mock.patch.object(volume_types, 'get_volume_type')
def test_get_violin_extra_spec(self,
m_get_volume_type,
m_get_admin_context):
'''Volume_type extra specs are found successfully.'''
vol = VOLUME.copy()
vol['volume_type_id'] = 1
volume_type = {'extra_specs': {'violin:test_key': 'test_value'}}
m_get_admin_context.return_value = None
m_get_volume_type.return_value = volume_type
result = self.driver._get_volume_type_extra_spec(vol, 'test_key')
m_get_admin_context.assert_called_with()
m_get_volume_type.assert_called_with(None, vol['volume_type_id'])
self.assertEqual('test_value', result)

View File

@ -0,0 +1,563 @@
# Copyright 2015 Violin Memory, Inc.
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
"""
Tests for Violin Memory 7000 Series All-Flash Array Fibrechannel Driver
"""
import mock
from cinder import exception
from cinder import test
from cinder.tests.unit import fake_vmem_client as vmemclient
from cinder.volume import configuration as conf
from cinder.volume.drivers.violin import v7000_common
from cinder.volume.drivers.violin import v7000_fcp
VOLUME_ID = "abcdabcd-1234-abcd-1234-abcdeffedcba"
VOLUME = {
"name": "volume-" + VOLUME_ID,
"id": VOLUME_ID,
"display_name": "fake_volume",
"size": 2,
"host": "myhost",
"volume_type": None,
"volume_type_id": None,
}
SNAPSHOT_ID = "abcdabcd-1234-abcd-1234-abcdeffedcbb"
SNAPSHOT = {
"name": "snapshot-" + SNAPSHOT_ID,
"id": SNAPSHOT_ID,
"volume_id": VOLUME_ID,
"volume_name": "volume-" + VOLUME_ID,
"volume_size": 2,
"display_name": "fake_snapshot",
"volume": VOLUME,
}
SRC_VOL_ID = "abcdabcd-1234-abcd-1234-abcdeffedcbc"
SRC_VOL = {
"name": "volume-" + SRC_VOL_ID,
"id": SRC_VOL_ID,
"display_name": "fake_src_vol",
"size": 2,
"host": "myhost",
"volume_type": None,
"volume_type_id": None,
}
INITIATOR_IQN = "iqn.1111-22.org.debian:11:222"
CONNECTOR = {
"initiator": INITIATOR_IQN,
"host": "irrelevant",
'wwpns': ['50014380186b3f65', '50014380186b3f67'],
}
FC_TARGET_WWPNS = [
'31000024ff45fb22', '21000024ff45fb23',
'51000024ff45f1be', '41000024ff45f1bf'
]
FC_INITIATOR_WWPNS = [
'50014380186b3f65', '50014380186b3f67'
]
FC_FABRIC_MAP = {
'fabricA':
{'target_port_wwn_list': [FC_TARGET_WWPNS[0], FC_TARGET_WWPNS[1]],
'initiator_port_wwn_list': [FC_INITIATOR_WWPNS[0]]},
'fabricB':
{'target_port_wwn_list': [FC_TARGET_WWPNS[2], FC_TARGET_WWPNS[3]],
'initiator_port_wwn_list': [FC_INITIATOR_WWPNS[1]]}
}
FC_INITIATOR_TARGET_MAP = {
FC_INITIATOR_WWPNS[0]: [FC_TARGET_WWPNS[0], FC_TARGET_WWPNS[1]],
FC_INITIATOR_WWPNS[1]: [FC_TARGET_WWPNS[2], FC_TARGET_WWPNS[3]]
}
PHY_DEVICES_RESPONSE = {
'data':
{'physical_devices':
[{'availsize': 1099504287744,
'availsize_mb': 524284,
'category': 'Virtual Device',
'connection_type': 'block',
'firmware': 'v1.0',
'guid': '3cc4d6dd-166d-77d2-4967-00005463f597',
'inquiry_string': '000002122b000032BKSC OTHDISK-MFCN01 v1.0',
'is_foreign': True,
'name': 'BKSC:OTHDISK-MFCN01.000',
'object_id': '84b834fb-1f4d-5d3b-b7ae-5796f9868151',
'owner': 'google-public-dns-a.google.com',
'pool': None,
'product': 'OTHDISK-MFCN01',
'scsi_address':
{'adapter': '98',
'channel': '0',
'id': '0',
'lun': '0',
'object_id': '6e0106fc-9c1c-52a2-95c9-396b7a653ac1'},
'size': 1099504287744,
'size_mb': 1048569,
'type': 'Direct-Access',
'usedsize': 0,
'usedsize_mb': 0,
'vendor': 'BKSC',
'wwid': 'BKSC OTHDISK-MFCN01 v1.0-0-0-00'},
{'availsize': 1099504287744,
'availsize_mb': 524284,
'category': 'Virtual Device',
'connection_type': 'block',
'firmware': 'v1.0',
'guid': '283b2694-192b-4745-6768-00005463f673',
'inquiry_string': '000002122b000032BKSC OTHDISK-MFCN08 v1.0',
'is_foreign': False,
'name': 'BKSC:OTHDISK-MFCN08.000',
'object_id': '8555b888-bf43-5083-a433-f0c7b0282370',
'owner': 'google-public-dns-a.google.com',
'pool':
{'name': 'mga-pool',
'object_id': '0818d3de-4437-535f-9cac-cc100a2c9313'},
'product': 'OTHDISK-MFCN08',
'scsi_address':
{'adapter': '98',
'channel': '0',
'id': '11',
'lun': '0',
'object_id': '6e0106fc-9c1c-52a2-95c9-396b7a653ac1'},
'size': 1099504287744,
'size_mb': 1048569,
'type': 'Direct-Access',
'usedsize': 0,
'usedsize_mb': 0,
'vendor': 'BKSC',
'wwid': 'BKSC OTHDISK-MFCN08 v1.0-0-0-00'},
{'availsize': 1099504287744,
'availsize_mb': 1048569,
'category': 'Virtual Device',
'connection_type': 'block',
'firmware': 'v1.0',
'guid': '7f47db19-019c-707d-0df1-00005463f949',
'inquiry_string': '000002122b000032BKSC OTHDISK-MFCN09 v1.0',
'is_foreign': False,
'name': 'BKSC:OTHDISK-MFCN09.000',
'object_id': '62a98898-f8b8-5837-af2b-764f5a72e291',
'owner': 'a.b.c.d',
'pool':
{'name': 'mga-pool',
'object_id': '0818d3de-4437-535f-9cac-cc100a2c9313'},
'product': 'OTHDISK-MFCN09',
'scsi_address':
{'adapter': '98',
'channel': '0',
'id': '12',
'lun': '0',
'object_id': '6e0106fc-9c1c-52a2-95c9-396b7a653ac1'},
'size': 1099504287744,
'size_mb': 524284,
'type': 'Direct-Access',
'usedsize': 0,
'usedsize_mb': 0,
'vendor': 'BKSC',
'wwid': 'BKSC OTHDISK-MFCN09 v1.0-0-0-00'}],
'total_physical_devices': 3},
'msg': 'Successful',
'success': True
}
# The FC_INFO dict returned by the backend is keyed on
# object_id of the FC adapter and the values are the
# wwmns
FC_INFO = {
'1a3cdb6a-383d-5ba6-a50b-4ba598074510': ['2100001b9745e25e'],
'4a6bc10a-5547-5cc0-94f2-76222a8f8dff': ['2100001b9745e230'],
'b21bfff5-d89e-51ff-9920-d990a061d722': ['2100001b9745e25f'],
'b508cc6b-f78a-51f9-81cf-47c1aaf53dd1': ['2100001b9745e231']
}
CLIENT_INFO = {
'FCPolicy':
{'AS400enabled': False,
'VSAenabled': False,
'initiatorWWPNList': ['50-01-43-80-18-6b-3f-66',
'50-01-43-80-18-6b-3f-64']},
'FibreChannelDevices':
[{'access': 'ReadWrite',
'id': 'v0000004',
'initiatorWWPN': '*',
'lun': '8',
'name': 'abcdabcd-1234-abcd-1234-abcdeffedcba',
'sizeMB': 10240,
'targetWWPN': '*',
'type': 'SAN'}]
}
CLIENT_INFO1 = {
'FCPolicy':
{'AS400enabled': False,
'VSAenabled': False,
'initiatorWWPNList': ['50-01-43-80-18-6b-3f-66',
'50-01-43-80-18-6b-3f-64']},
'FibreChannelDevices': []
}
class V7000FCPDriverTestCase(test.TestCase):
"""Test cases for VMEM FCP driver."""
def setUp(self):
super(V7000FCPDriverTestCase, self).setUp()
self.conf = self.setup_configuration()
self.driver = v7000_fcp.V7000FCPDriver(configuration=self.conf)
self.driver.common.container = 'myContainer'
self.driver.device_id = 'ata-VIOLIN_MEMORY_ARRAY_23109R00000022'
self.driver.gateway_fc_wwns = FC_TARGET_WWPNS
self.stats = {}
self.driver.set_initialized()
def tearDown(self):
super(V7000FCPDriverTestCase, self).tearDown()
def setup_configuration(self):
config = mock.Mock(spec=conf.Configuration)
config.volume_backend_name = 'v7000_fcp'
config.san_ip = '8.8.8.8'
config.san_login = 'admin'
config.san_password = ''
config.san_thin_provision = False
config.san_is_local = False
config.request_timeout = 300
config.container = 'myContainer'
return config
def setup_mock_concerto(self, m_conf=None):
"""Create a fake Concerto communication object."""
_m_concerto = mock.Mock(name='Concerto',
version='1.1.1',
spec=vmemclient.mock_client_conf)
if m_conf:
_m_concerto.configure_mock(**m_conf)
return _m_concerto
@mock.patch.object(v7000_common.V7000Common, 'check_for_setup_error')
def test_check_for_setup_error(self, m_setup_func):
"""No setup errors are found."""
result = self.driver.check_for_setup_error()
m_setup_func.assert_called_with()
self.assertIsNone(result)
@mock.patch.object(v7000_common.V7000Common, 'check_for_setup_error')
def test_check_for_setup_error_no_wwn_config(self, m_setup_func):
"""No wwns were found during setup."""
self.driver.gateway_fc_wwns = []
failure = exception.ViolinInvalidBackendConfig
self.assertRaises(failure, self.driver.check_for_setup_error)
def test_create_volume(self):
"""Volume created successfully."""
self.driver.common._create_lun = mock.Mock()
result = self.driver.create_volume(VOLUME)
self.driver.common._create_lun.assert_called_with(VOLUME)
self.assertIsNone(result)
def test_create_volume_from_snapshot(self):
self.driver.common._create_volume_from_snapshot = mock.Mock()
result = self.driver.create_volume_from_snapshot(VOLUME, SNAPSHOT)
self.driver.common._create_volume_from_snapshot.assert_called_with(
SNAPSHOT, VOLUME)
self.assertIsNone(result)
def test_create_cloned_volume(self):
self.driver.common._create_lun_from_lun = mock.Mock()
result = self.driver.create_cloned_volume(VOLUME, SRC_VOL)
self.driver.common._create_lun_from_lun.assert_called_with(
SRC_VOL, VOLUME)
self.assertIsNone(result)
def test_delete_volume(self):
"""Volume deleted successfully."""
self.driver.common._delete_lun = mock.Mock()
result = self.driver.delete_volume(VOLUME)
self.driver.common._delete_lun.assert_called_with(VOLUME)
self.assertIsNone(result)
def test_extend_volume(self):
"""Volume extended successfully."""
new_size = 10
self.driver.common._extend_lun = mock.Mock()
result = self.driver.extend_volume(VOLUME, new_size)
self.driver.common._extend_lun.assert_called_with(VOLUME, new_size)
self.assertIsNone(result)
def test_create_snapshot(self):
self.driver.common._create_lun_snapshot = mock.Mock()
result = self.driver.create_snapshot(SNAPSHOT)
self.driver.common._create_lun_snapshot.assert_called_with(SNAPSHOT)
self.assertIsNone(result)
def test_delete_snapshot(self):
self.driver.common._delete_lun_snapshot = mock.Mock()
result = self.driver.delete_snapshot(SNAPSHOT)
self.driver.common._delete_lun_snapshot.assert_called_with(SNAPSHOT)
self.assertIsNone(result)
def test_get_volume_stats(self):
self.driver._update_volume_stats = mock.Mock()
self.driver._update_volume_stats()
result = self.driver.get_volume_stats(True)
self.driver._update_volume_stats.assert_called_with()
self.assertEqual(self.driver.stats, result)
def test_update_volume_stats(self):
"""Makes a mock query to the backend to collect
stats on all physical devices.
"""
backend_name = self.conf.volume_backend_name
vendor_name = "Violin Memory, Inc."
tot_gb = 2046
free_gb = 1022
phy_devices = "/batch/physicalresource/physicaldevice"
conf = {
'basic.get.side_effect': [PHY_DEVICES_RESPONSE, ],
}
self.driver.common.vmem_mg = self.setup_mock_concerto(m_conf=conf)
result = self.driver._update_volume_stats()
calls = [mock.call(phy_devices)]
self.driver.common.vmem_mg.basic.get.assert_has_calls(calls)
self.assertEqual(tot_gb, self.driver.stats['total_capacity_gb'])
self.assertEqual(free_gb, self.driver.stats['free_capacity_gb'])
self.assertEqual(backend_name,
self.driver.stats['volume_backend_name'])
self.assertEqual(vendor_name, self.driver.stats['vendor_name'])
self.assertIsNone(result)
def test_get_active_fc_targets(self):
"""Makes a mock query to the backend to collect
all the physical adapters and extract the WWNs
"""
conf = {
'adapter.get_fc_info.return_value': FC_INFO,
}
self.driver.common.vmem_mg = self.setup_mock_concerto(m_conf=conf)
result = self.driver._get_active_fc_targets()
self.assertEqual(['2100001b9745e230', '2100001b9745e25f',
'2100001b9745e231', '2100001b9745e25e'],
result)
def test_initialize_connection(self):
lun_id = 1
target_wwns = self.driver.gateway_fc_wwns
init_targ_map = {}
conf = {
'client.create_client.return_value': None,
}
self.driver.common.vmem_mg = self.setup_mock_concerto(m_conf=conf)
self.driver._export_lun = mock.Mock(return_value=lun_id)
self.driver._build_initiator_target_map = mock.Mock(
return_value=(target_wwns, init_targ_map))
props = self.driver.initialize_connection(VOLUME, CONNECTOR)
self.driver.common.vmem_mg.client.create_client.assert_called_with(
name=CONNECTOR['host'], proto='FC', fc_wwns=CONNECTOR['wwpns'])
self.driver._export_lun.assert_called_with(VOLUME, CONNECTOR)
self.driver._build_initiator_target_map.assert_called_with(
CONNECTOR)
self.assertEqual(props['driver_volume_type'], "fibre_channel")
self.assertEqual(props['data']['target_discovered'], True)
self.assertEqual(props['data']['target_wwn'],
self.driver.gateway_fc_wwns)
self.assertEqual(props['data']['target_lun'], lun_id)
def test_terminate_connection(self):
target_wwns = self.driver.gateway_fc_wwns
init_targ_map = {}
self.driver.common.vmem_mg = self.setup_mock_concerto()
self.driver._unexport_lun = mock.Mock()
self.driver._is_initiator_connected_to_array = mock.Mock(
return_value=False)
self.driver._build_initiator_target_map = mock.Mock(
return_value=(target_wwns, init_targ_map))
props = self.driver.terminate_connection(VOLUME, CONNECTOR)
self.driver._unexport_lun.assert_called_with(VOLUME, CONNECTOR)
self.driver._is_initiator_connected_to_array.assert_called_with(
CONNECTOR)
self.driver._build_initiator_target_map.assert_called_with(
CONNECTOR)
self.assertEqual("fibre_channel", props['driver_volume_type'])
self.assertEqual(target_wwns, props['data']['target_wwn'])
self.assertEqual(init_targ_map, props['data']['initiator_target_map'])
def test_export_lun(self):
lun_id = '1'
response = {'success': True, 'msg': 'Assign SAN client successfully'}
conf = {
'client.get_client_info.return_value': CLIENT_INFO,
}
self.driver.common.vmem_mg = self.setup_mock_concerto(m_conf=conf)
self.driver.common._send_cmd_and_verify = mock.Mock(
return_value=response)
self.driver._get_lun_id = mock.Mock(return_value=lun_id)
result = self.driver._export_lun(VOLUME, CONNECTOR)
self.driver.common._send_cmd_and_verify.assert_called_with(
self.driver.common.vmem_mg.lun.assign_lun_to_client,
self.driver._is_lun_id_ready,
'Assign SAN client successfully',
[VOLUME['id'], CONNECTOR['host'], "ReadWrite"],
[VOLUME['id'], CONNECTOR['host']])
self.driver._get_lun_id.assert_called_with(
VOLUME['id'], CONNECTOR['host'])
self.assertEqual(lun_id, result)
def test_export_lun_fails_with_exception(self):
lun_id = '1'
response = {'status': False, 'msg': 'Generic error'}
failure = exception.ViolinBackendErr
self.driver.common.vmem_mg = self.setup_mock_concerto()
self.driver.common._send_cmd_and_verify = mock.Mock(
side_effect=exception.ViolinBackendErr(response['msg']))
self.driver._get_lun_id = mock.Mock(return_value=lun_id)
self.assertRaises(failure, self.driver._export_lun, VOLUME, CONNECTOR)
def test_unexport_lun(self):
response = {'success': True, 'msg': 'Unassign SAN client successfully'}
self.driver.common.vmem_mg = self.setup_mock_concerto()
self.driver.common._send_cmd = mock.Mock(
return_value=response)
result = self.driver._unexport_lun(VOLUME, CONNECTOR)
self.driver.common._send_cmd.assert_called_with(
self.driver.common.vmem_mg.lun.unassign_client_lun,
"Unassign SAN client successfully",
VOLUME['id'], CONNECTOR['host'], True)
self.assertIsNone(result)
def test_get_lun_id(self):
conf = {
'client.get_client_info.return_value': CLIENT_INFO,
}
self.driver.common.vmem_mg = self.setup_mock_concerto(m_conf=conf)
result = self.driver._get_lun_id(VOLUME['id'], CONNECTOR['host'])
self.assertEqual(8, result)
def test_is_lun_id_ready(self):
lun_id = '1'
self.driver.common.vmem_mg = self.setup_mock_concerto()
self.driver._get_lun_id = mock.Mock(return_value=lun_id)
result = self.driver._is_lun_id_ready(
VOLUME['id'], CONNECTOR['host'])
self.assertTrue(result)
def test_build_initiator_target_map(self):
"""Successfully build a map when zoning is enabled."""
expected_targ_wwns = FC_TARGET_WWPNS
self.driver.lookup_service = mock.Mock()
(self.driver.lookup_service.get_device_mapping_from_network.
return_value) = FC_FABRIC_MAP
result = self.driver._build_initiator_target_map(CONNECTOR)
(targ_wwns, init_targ_map) = result
(self.driver.lookup_service.get_device_mapping_from_network.
assert_called_with(CONNECTOR['wwpns'], self.driver.gateway_fc_wwns))
self.assertEqual(set(expected_targ_wwns), set(targ_wwns))
i = FC_INITIATOR_WWPNS[0]
self.assertIn(FC_TARGET_WWPNS[0], init_targ_map[i])
self.assertIn(FC_TARGET_WWPNS[1], init_targ_map[i])
self.assertEqual(2, len(init_targ_map[i]))
i = FC_INITIATOR_WWPNS[1]
self.assertIn(FC_TARGET_WWPNS[2], init_targ_map[i])
self.assertIn(FC_TARGET_WWPNS[3], init_targ_map[i])
self.assertEqual(2, len(init_targ_map[i]))
self.assertEqual(2, len(init_targ_map))
def test_build_initiator_target_map_no_lookup_service(self):
"""Successfully build a map when zoning is disabled."""
expected_targ_wwns = FC_TARGET_WWPNS
expected_init_targ_map = {
CONNECTOR['wwpns'][0]: FC_TARGET_WWPNS,
CONNECTOR['wwpns'][1]: FC_TARGET_WWPNS
}
self.driver.lookup_service = None
targ_wwns, init_targ_map = self.driver._build_initiator_target_map(
CONNECTOR)
self.assertEqual(expected_targ_wwns, targ_wwns)
self.assertEqual(expected_init_targ_map, init_targ_map)
def test_is_initiator_connected_to_array(self):
"""Successfully finds an initiator with remaining active session."""
conf = {
'client.get_client_info.return_value': CLIENT_INFO,
}
self.driver.common.vmem_mg = self.setup_mock_concerto(m_conf=conf)
self.assertTrue(self.driver._is_initiator_connected_to_array(
CONNECTOR))
self.driver.common.vmem_mg.client.get_client_info.assert_called_with(
CONNECTOR['host'])
def test_is_initiator_connected_to_array_empty_response(self):
"""Successfully finds no initiators with remaining active sessions."""
conf = {
'client.get_client_info.return_value': CLIENT_INFO1
}
self.driver.common.vmem_mg = self.setup_mock_concerto(m_conf=conf)
self.assertFalse(self.driver._is_initiator_connected_to_array(
CONNECTOR))

View File

@ -0,0 +1,854 @@
# Copyright 2015 Violin Memory, Inc.
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
"""
Violin Memory 7000 Series All-Flash Array Common Driver for OpenStack Cinder
Provides common (ie., non-protocol specific) management functions for
V7000 series flash arrays.
Backend array communication is handled via VMEM's python library
called 'vmemclient'.
NOTE: this driver file requires the use of synchronization points for
certain types of backend operations, and as a result may not work
properly in an active-active HA configuration. See OpenStack Cinder
driver documentation for more information.
"""
import math
import re
import six
import time
from oslo_config import cfg
from oslo_log import log as logging
from oslo_utils import units
from cinder import context
from cinder.db.sqlalchemy import api
from cinder import exception
from cinder.i18n import _, _LE, _LI
from cinder.openstack.common import loopingcall
from cinder import utils
from cinder.volume import volume_types
LOG = logging.getLogger(__name__)
try:
import vmemclient
except ImportError:
vmemclient = None
else:
LOG.info(_LI("Running with vmemclient version: %s"),
vmemclient.__version__)
CONCERTO_SUPPORTED_VERSION_PATTERNS = ['Version 7.[0-9].?[0-9]?']
CONCERTO_DEFAULT_PRIORITY = 'medium'
CONCERTO_DEFAULT_SRA_POLICY = 'preserveAll'
CONCERTO_DEFAULT_SRA_ENABLE_EXPANSION = True
CONCERTO_DEFAULT_SRA_EXPANSION_THRESHOLD = 50
CONCERTO_DEFAULT_SRA_EXPANSION_INCREMENT = '1024MB'
CONCERTO_DEFAULT_SRA_EXPANSION_MAX_SIZE = None
CONCERTO_DEFAULT_SRA_ENABLE_SHRINK = False
CONCERTO_DEFAULT_POLICY_MAX_SNAPSHOTS = 1000
CONCERTO_DEFAULT_POLICY_RETENTION_MODE = 'All'
violin_opts = [
cfg.IntOpt('violin_request_timeout',
default=300,
help='Global backend request timeout, in seconds.'),
]
CONF = cfg.CONF
CONF.register_opts(violin_opts)
class V7000Common(object):
"""Contains common code for the Violin V7000 drivers."""
def __init__(self, config):
self.vmem_mg = None
self.container = ""
self.config = config
def do_setup(self, context):
"""Any initialization the driver does while starting."""
if not self.config.san_ip:
raise exception.InvalidInput(
reason=_('Gateway VIP is not set'))
self.vmem_mg = vmemclient.open(self.config.san_ip,
self.config.san_login,
self.config.san_password,
keepalive=True)
if self.vmem_mg is None:
msg = _('Failed to connect to array')
raise exception.VolumeBackendAPIException(data=msg)
def check_for_setup_error(self):
"""Returns an error if prerequisites aren't met."""
if vmemclient is None:
msg = _('vmemclient python library not found')
raise exception.VolumeBackendAPIException(data=msg)
LOG.info(_LI("CONCERTO version: %s"), self.vmem_mg.version)
if not self._is_supported_vmos_version(self.vmem_mg.version):
msg = _('CONCERTO version is not supported')
raise exception.ViolinInvalidBackendConfig(reason=msg)
@utils.synchronized('vmem-lun')
def _create_lun(self, volume):
"""Creates a new lun.
:param volume: volume object provided by the Manager
"""
thin_lun = False
dedup = False
size_mb = volume['size'] * units.Ki
full_size_mb = size_mb
pool = None
LOG.debug("Creating LUN %(name)s, %(size)s MB.",
{'name': volume['name'], 'size': size_mb})
if self.config.san_thin_provision:
thin_lun = True
# Set the actual allocation size for thin lun
# default here is 10%
size_mb = size_mb / 10
typeid = volume['volume_type_id']
if typeid:
# extra_specs with thin specified overrides san_thin_provision
spec_value = self._get_volume_type_extra_spec(volume, "thin")
if spec_value and spec_value.lower() == "true":
thin_lun = True
# Set the actual allocation size for thin lun
# default here is 10%
size_mb = size_mb / 10
spec_value = self._get_volume_type_extra_spec(volume, "dedup")
if spec_value and spec_value.lower() == "true":
dedup = True
# A dedup lun is always a thin lun
thin_lun = True
# Set the actual allocation size for thin lun
# default here is 10%. The actual allocation may
# different, depending on other factors
size_mb = full_size_mb / 10
# Extract the storage_pool name if one is specified
pool = self._get_violin_extra_spec(volume, "storage_pool")
try:
# Note: In the following create_lun command for setting up a dedup
# or thin lun the size_mb parameter is ignored and 10% of the
# full_size_mb specified is the size actually allocated to
# the lun. full_size_mb is the size the lun is allowed to
# grow. On the other hand, if it is a thick lun, the
# full_size_mb is ignored and size_mb is the actual
# allocated size of the lun.
self._send_cmd(self.vmem_mg.lun.create_lun,
"Create resource successfully.",
volume['id'], size_mb, dedup,
thin_lun, full_size_mb, storage_pool=pool)
except Exception:
LOG.exception(_LE("Lun create for %s failed!"), volume['id'])
raise
@utils.synchronized('vmem-lun')
def _delete_lun(self, volume):
"""Deletes a lun.
:param volume: volume object provided by the Manager
"""
success_msgs = ['Delete resource successfully', '']
LOG.debug("Deleting lun %s.", volume['id'])
try:
# If the LUN has ever had a snapshot, it has an SRA and
# policy that must be deleted first.
self._delete_lun_snapshot_bookkeeping(volume['id'])
# TODO(rdl) force the delete for now to deal with pending
# snapshot issues. Should revisit later for a better fix.
self._send_cmd(self.vmem_mg.lun.delete_lun,
success_msgs, volume['id'], True)
except exception.VolumeBackendAPIException:
LOG.exception(_LE("Lun %s has dependent snapshots, "
"skipping lun deletion."), volume['id'])
raise exception.VolumeIsBusy(volume_name=volume['id'])
except Exception:
LOG.exception(_LE("Lun delete for %s failed!"), volume['id'])
raise
def _extend_lun(self, volume, new_size):
"""Extend an existing volume's size.
:param volume: volume object provided by the Manager
:param new_size: new size in GB to be applied
"""
v = self.vmem_mg
typeid = volume['volume_type_id']
if typeid:
spec_value = self._get_volume_type_extra_spec(volume, "dedup")
if spec_value and spec_value.lower() == "true":
# A Dedup lun's size cannot be modified in Concerto.
msg = _('Dedup luns cannot be extended')
raise exception.VolumeDriverException(message=msg)
size_mb = volume['size'] * units.Ki
new_size_mb = new_size * units.Ki
# Concerto lun extend requires number of MB to increase size by,
# not the final size value.
#
delta_mb = new_size_mb - size_mb
LOG.debug("Extending lun %(id)s, from %(size)s to %(new_size)s MB.",
{'id': volume['id'], 'size': size_mb,
'new_size': new_size_mb})
try:
self._send_cmd(v.lun.extend_lun,
"Expand resource successfully",
volume['id'], delta_mb)
except Exception:
LOG.exception(_LE("LUN extend failed!"))
raise
def _create_lun_snapshot(self, snapshot):
"""Create a new cinder snapshot on a volume.
This maps onto a Concerto 'timemark', but we must always first
ensure that a snapshot resource area (SRA) exists, and that a
snapshot policy exists.
:param snapshot: cinder snapshot object provided by the Manager
Exceptions:
VolumeBackendAPIException: If SRA could not be created, or
snapshot policy could not be created
RequestRetryTimeout: If backend could not complete the request
within the allotted timeout.
ViolinBackendErr: If backend reports an error during the
create snapshot phase.
"""
cinder_volume_id = snapshot['volume_id']
cinder_snapshot_id = snapshot['id']
LOG.debug("Creating LUN snapshot %(snap_id)s on volume "
"%(vol_id)s %(dpy_name)s.",
{'snap_id': cinder_snapshot_id,
'vol_id': cinder_volume_id,
'dpy_name': snapshot['display_name']})
self._ensure_snapshot_resource_area(cinder_volume_id)
self._ensure_snapshot_policy(cinder_volume_id)
try:
self._send_cmd(
self.vmem_mg.snapshot.create_lun_snapshot,
"Create TimeMark successfully",
lun=cinder_volume_id,
comment=self._compress_snapshot_id(cinder_snapshot_id),
priority=CONCERTO_DEFAULT_PRIORITY,
enable_notification=False)
except Exception:
LOG.exception(_LE("Lun create snapshot for "
"volume %(vol)s snapshot %(snap)s failed!"),
{'vol': cinder_volume_id,
'snap': cinder_snapshot_id})
raise
def _delete_lun_snapshot(self, snapshot):
"""Delete the specified cinder snapshot.
:param snapshot: cinder snapshot object provided by the Manager
Exceptions:
RequestRetryTimeout: If backend could not complete the request
within the allotted timeout.
ViolinBackendErr: If backend reports an error during the
delete snapshot phase.
"""
cinder_volume_id = snapshot['volume_id']
cinder_snapshot_id = snapshot['id']
LOG.debug("Deleting snapshot %(snap_id)s on volume "
"%(vol_id)s %(dpy_name)s",
{'snap_id': cinder_snapshot_id,
'vol_id': cinder_volume_id,
'dpy_name': snapshot['display_name']})
try:
self._send_cmd(
self.vmem_mg.snapshot.delete_lun_snapshot,
"Delete TimeMark successfully",
lun=cinder_volume_id,
comment=self._compress_snapshot_id(cinder_snapshot_id))
except Exception:
LOG.exception(_LE("Lun delete snapshot for "
"volume %(vol)s snapshot %(snap)s failed!"),
{'vol': cinder_volume_id,
'snap': cinder_snapshot_id})
raise
def _create_volume_from_snapshot(self, snapshot, volume):
"""Create a new cinder volume from a given snapshot of a lun
This maps onto a Concerto 'copy snapshot to lun'. Concerto
creates the lun and then copies the snapshot into it.
:param snapshot: cinder snapshot object provided by the Manager
:param volume: cinder volume to be created
"""
cinder_volume_id = volume['id']
cinder_snapshot_id = snapshot['id']
pool = None
result = None
LOG.debug("Copying snapshot %(snap_id)s onto volume %(vol_id)s.",
{'snap_id': cinder_snapshot_id,
'vol_id': cinder_volume_id})
typeid = volume['volume_type_id']
if typeid:
pool = self._get_violin_extra_spec(volume, "storage_pool")
try:
result = self.vmem_mg.lun.copy_snapshot_to_new_lun(
source_lun=snapshot['volume_id'],
source_snapshot_comment=
self._compress_snapshot_id(cinder_snapshot_id),
destination=cinder_volume_id,
storage_pool=pool)
if not result['success']:
self._check_error_code(result)
except Exception:
LOG.exception(_LE("Copy snapshot to volume for "
"snapshot %(snap)s volume %(vol)s failed!"),
{'snap': cinder_snapshot_id,
'vol': cinder_volume_id})
raise
# get the destination lun info and extract virtualdeviceid
info = self.vmem_mg.lun.get_lun_info(object_id=result['object_id'])
self._wait_for_lun_or_snap_copy(
snapshot['volume_id'], dest_vdev_id=info['virtualDeviceID'])
def _create_lun_from_lun(self, src_vol, dest_vol):
"""Copy the contents of a lun to a new lun (i.e., full clone).
:param src_vol: cinder volume to clone
:param dest_vol: cinder volume to be created
"""
pool = None
result = None
LOG.debug("Copying lun %(src_vol_id)s onto lun %(dest_vol_id)s.",
{'src_vol_id': src_vol['id'],
'dest_vol_id': dest_vol['id']})
# Extract the storage_pool name if one is specified
typeid = dest_vol['volume_type_id']
if typeid:
pool = self._get_violin_extra_spec(dest_vol, "storage_pool")
try:
# in order to do a full clone the source lun must have a
# snapshot resource
self._ensure_snapshot_resource_area(src_vol['id'])
result = self.vmem_mg.lun.copy_lun_to_new_lun(
source=src_vol['id'], destination=dest_vol['id'],
storage_pool=pool)
if not result['success']:
self._check_error_code(result)
except Exception:
LOG.exception(_LE("Create new lun from lun for source "
"%(src)s => destination %(dest)s failed!"),
{'src': src_vol['id'], 'dest': dest_vol['id']})
raise
self._wait_for_lun_or_snap_copy(
src_vol['id'], dest_obj_id=result['object_id'])
def _send_cmd(self, request_func, success_msgs, *args, **kwargs):
"""Run an XG request function, and retry as needed.
The request will be retried until it returns a success
message, a failure message, or the global request timeout is
hit.
This wrapper is meant to deal with backend requests that can
fail for any variety of reasons, for instance, when the system
is already busy handling other LUN requests. If there is no
space left, or other "fatal" errors are returned (see
_fatal_error_code() for a list of all known error conditions).
:param request_func: XG api method to call
:param success_msgs: Success messages expected from the backend
:param *args: argument array to be passed to the request_func
:param **kwargs: argument dictionary to be passed to request_func
:returns: the response dict from the last XG call
"""
resp = {}
start = time.time()
done = False
if isinstance(success_msgs, basestring):
success_msgs = [success_msgs]
while not done:
if time.time() - start >= self.config.violin_request_timeout:
raise exception.ViolinRequestRetryTimeout(
timeout=self.config.violin_request_timeout)
resp = request_func(*args, **kwargs)
if not resp['msg']:
# XG requests will return None for a message if no message
# string is passed in the raw response
resp['msg'] = ''
for msg in success_msgs:
if resp['success'] and msg in resp['msg']:
done = True
break
if not resp['success']:
self._check_error_code(resp)
done = True
break
return resp
def _send_cmd_and_verify(self, request_func, verify_func,
request_success_msgs='', rargs=None, vargs=None):
"""Run an XG request function, retry if needed, and verify success.
If the verification fails, then retry the request/verify cycle
until both functions are successful, the request function
returns a failure message, or the global request timeout is
hit.
This wrapper is meant to deal with backend requests that can
fail for any variety of reasons, for instance, when the system
is already busy handling other LUN requests. It is also smart
enough to give up if clustering is down (eg no HA available),
there is no space left, or other "fatal" errors are returned
(see _fatal_error_code() for a list of all known error
conditions).
:param request_func: XG api method to call
:param verify_func: function call to verify request was completed
:param request_success_msg: Success message expected for request_func
:param *rargs: argument array to be passed to request_func
:param *vargs: argument array to be passed to verify_func
:returns: the response dict from the last XG call
"""
resp = {}
start = time.time()
request_needed = True
verify_needed = True
if isinstance(request_success_msgs, basestring):
request_success_msgs = [request_success_msgs]
rargs = rargs if rargs else []
vargs = vargs if vargs else []
while request_needed or verify_needed:
if time.time() - start >= self.config.violin_request_timeout:
raise exception.ViolinRequestRetryTimeout(
timeout=self.config.violin_request_timeout)
if request_needed:
resp = request_func(*rargs)
if not resp['msg']:
# XG requests will return None for a message if no message
# string is passed in the raw response
resp['msg'] = ''
for msg in request_success_msgs:
if resp['success'] and msg in resp['msg']:
request_needed = False
break
if not resp['success']:
self._check_error_code(resp)
request_needed = False
elif verify_needed:
success = verify_func(*vargs)
if success:
# XG verify func was completed
verify_needed = False
return resp
def _ensure_snapshot_resource_area(self, volume_id):
"""Make sure concerto snapshot resource area exists on volume.
:param volume_id: Cinder volume ID corresponding to the backend LUN
Exceptions:
VolumeBackendAPIException: if cinder volume does not exist
on backnd, or SRA could not be created.
"""
ctxt = context.get_admin_context()
volume = api.volume_get(ctxt, volume_id)
pool = None
if not volume:
msg = (_("Failed to ensure snapshot resource area, could not "
"locate volume for id %s") % volume_id)
raise exception.VolumeBackendAPIException(data=msg)
if not self.vmem_mg.snapshot.lun_has_a_snapshot_resource(
lun=volume_id):
# Per Concerto documentation, the SRA size should be computed
# as follows
# Size-of-original-LUN Reserve for SRA
# < 500MB 100%
# 500MB to 2G 50%
# >= 2G 20%
# Note: cinder volume.size is in GB, vmemclient wants MB.
lun_size_mb = volume['size'] * units.Ki
if lun_size_mb < 500:
snap_size_mb = lun_size_mb
elif lun_size_mb < 2000:
snap_size_mb = 0.5 * lun_size_mb
else:
snap_size_mb = 0.2 * lun_size_mb
snap_size_mb = int(math.ceil(snap_size_mb))
typeid = volume['volume_type_id']
if typeid:
pool = self._get_violin_extra_spec(volume, "storage_pool")
LOG.debug("Creating SRA of %(ssmb)sMB for lun of %(lsmb)sMB "
"on %(vol_id)s.",
{'ssmb': snap_size_mb,
'lsmb': lun_size_mb,
'vol_id': volume_id})
res = self.vmem_mg.snapshot.create_snapshot_resource(
lun=volume_id,
size=snap_size_mb,
enable_notification=False,
policy=CONCERTO_DEFAULT_SRA_POLICY,
enable_expansion=CONCERTO_DEFAULT_SRA_ENABLE_EXPANSION,
expansion_threshold=CONCERTO_DEFAULT_SRA_EXPANSION_THRESHOLD,
expansion_increment=CONCERTO_DEFAULT_SRA_EXPANSION_INCREMENT,
expansion_max_size=CONCERTO_DEFAULT_SRA_EXPANSION_MAX_SIZE,
enable_shrink=CONCERTO_DEFAULT_SRA_ENABLE_SHRINK,
storage_pool=pool)
if (not res['success']):
msg = (_("Failed to create snapshot resource area on "
"volume %(vol)s: %(res)s.") %
{'vol': volume_id, 'res': res['msg']})
raise exception.VolumeBackendAPIException(data=msg)
def _ensure_snapshot_policy(self, volume_id):
"""Ensure concerto snapshot policy exists on cinder volume.
A snapshot policy is required by concerto in order to create snapshots.
:param volume_id: Cinder volume ID corresponding to the backend LUN
Exceptions:
VolumeBackendAPIException: when snapshot policy cannot be created.
"""
if not self.vmem_mg.snapshot.lun_has_a_snapshot_policy(
lun=volume_id):
res = self.vmem_mg.snapshot.create_snapshot_policy(
lun=volume_id,
max_snapshots=CONCERTO_DEFAULT_POLICY_MAX_SNAPSHOTS,
enable_replication=False,
enable_snapshot_schedule=False,
enable_cdp=False,
retention_mode=CONCERTO_DEFAULT_POLICY_RETENTION_MODE)
if not res['success']:
msg = (_(
"Failed to create snapshot policy on "
"volume %(vol)s: %(res)s.") %
{'vol': volume_id, 'res': res['msg']})
raise exception.VolumeBackendAPIException(data=msg)
def _delete_lun_snapshot_bookkeeping(self, volume_id):
"""Clear residual snapshot support resources from LUN.
Exceptions:
VolumeBackendAPIException: If snapshots still exist on the LUN.
"""
# Make absolutely sure there are no snapshots present
try:
snaps = self.vmem_mg.snapshot.get_snapshots(volume_id)
if len(snaps) > 0:
msg = (_("Cannot delete LUN %s while snapshots exist.") %
volume_id)
raise exception.VolumeBackendAPIException(data=msg)
except vmemclient.core.error.NoMatchingObjectIdError:
pass
except vmemclient.core.error.MissingParameterError:
pass
try:
res = self.vmem_mg.snapshot.delete_snapshot_policy(
lun=volume_id)
if not res['success']:
if 'TimeMark is disabled' in res['msg']:
LOG.debug("Verified no snapshot policy is on volume %s.",
volume_id)
else:
msg = (_("Unable to delete snapshot policy on "
"volume %s.") % volume_id)
raise exception.VolumeBackendAPIException(data=msg)
else:
LOG.debug("Deleted snapshot policy on volume "
"%(vol)s, result %(res)s.",
{'vol': volume_id, 'res': res})
except vmemclient.core.error.NoMatchingObjectIdError:
LOG.debug("Verified no snapshot policy present on volume %s.",
volume_id)
pass
try:
res = self.vmem_mg.snapshot.delete_snapshot_resource(
lun=volume_id)
LOG.debug("Deleted snapshot resource area on "
"volume %(vol)s, result %(res)s.",
{'vol': volume_id, 'res': res})
except vmemclient.core.error.NoMatchingObjectIdError:
LOG.debug("Verified no snapshot resource area present on "
"volume %s.", volume_id)
pass
def _compress_snapshot_id(self, cinder_snap_id):
"""Compress cinder snapshot ID so it fits in backend.
Compresses to fit in 32-chars.
"""
return ''.join(six.text_type(cinder_snap_id).split('-'))
def _get_snapshot_from_lun_snapshots(
self, cinder_volume_id, cinder_snap_id):
"""Locate backend snapshot dict associated with cinder snapshot id.
:returns: Cinder snapshot dictionary if found, None otherwise.
"""
try:
snaps = self.vmem_mg.snapshot.get_snapshots(cinder_volume_id)
except vmemclient.core.error.NoMatchingObjectIdError:
return None
key = self._compress_snapshot_id(cinder_snap_id)
for s in snaps:
if s['comment'] == key:
# Remap return dict to its uncompressed form
s['comment'] = cinder_snap_id
return s
def _wait_for_lun_or_snap_copy(self, src_vol_id, dest_vdev_id=None,
dest_obj_id=None):
"""Poll to see when a lun or snap copy to a lun is complete.
:param src_vol_id: cinder volume ID of source volume
:param dest_vdev_id: virtual device ID of destination, for snap copy
:param dest_obj_id: lun object ID of destination, for lun copy
:returns: True if successful, False otherwise
"""
wait_id = None
wait_func = None
if dest_vdev_id:
wait_id = dest_vdev_id
wait_func = self.vmem_mg.snapshot.get_snapshot_copy_status
elif dest_obj_id:
wait_id = dest_obj_id
wait_func = self.vmem_mg.lun.get_lun_copy_status
else:
return False
def _loop_func():
LOG.debug("Entering _wait_for_lun_or_snap_copy loop: "
"vdev=%s, objid=%s", dest_vdev_id, dest_obj_id)
status = wait_func(src_vol_id)
if status[0] is None:
# pre-copy transient result, status=(None, None, 0)
LOG.debug("lun or snap copy prepping.")
pass
elif status[0] != wait_id:
# the copy must be complete since another lun is being copied
LOG.debug("lun or snap copy complete.")
raise loopingcall.LoopingCallDone(retvalue=True)
elif status[1] is not None:
# copy is in progress, status = ('12345', 1700, 10)
LOG.debug("MB copied:%d, percent done: %d.",
status[1], status[2])
pass
elif status[2] == 0:
# copy has just started, status = ('12345', None, 0)
LOG.debug("lun or snap copy started.")
pass
elif status[2] == 100:
# copy is complete, status = ('12345', None, 100)
LOG.debug("lun or snap copy complete.")
raise loopingcall.LoopingCallDone(retvalue=True)
else:
# unexpected case
LOG.debug("unexpected case (%{id}s, %{bytes}s, %{percent}s)",
{'id': six.text_type(status[0]),
'bytes': six.text_type(status[1]),
'percent': six.text_type(status[2])})
raise loopingcall.LoopingCallDone(retvalue=False)
timer = loopingcall.FixedIntervalLoopingCall(_loop_func)
success = timer.start(interval=1).wait()
return success
def _is_supported_vmos_version(self, version_string):
"""Check a version string for compatibility with OpenStack.
Compare a version string against the global regex of versions
compatible with OpenStack.
:param version_string: array's gateway version string
:returns: True if supported, false if not
"""
for pattern in CONCERTO_SUPPORTED_VERSION_PATTERNS:
if re.match(pattern, version_string):
return True
return False
def _check_error_code(self, response):
"""Raise an exception when backend returns certain errors.
Error codes returned from the backend have to be examined
individually. Not all of them are fatal. For example, lun attach
failing becase the client is already attached is not a fatal error.
:param response: a response dict result from the vmemclient request
"""
if "Error: 0x9001003c" in response['msg']:
# This error indicates a duplicate attempt to attach lun,
# non-fatal error
pass
elif "Error: 0x9002002b" in response['msg']:
# lun unexport failed - lun is not exported to any clients,
# non-fatal error
pass
elif "Error: 0x09010023" in response['msg']:
# lun delete failed - dependent snapshot copy in progress,
# fatal error
raise exception.ViolinBackendErr(message=response['msg'])
elif "Error: 0x09010048" in response['msg']:
# lun delete failed - dependent snapshots still exist,
# fatal error
raise exception.ViolinBackendErr(message=response['msg'])
elif "Error: 0x90010022" in response['msg']:
# lun create failed - lun with same name already exists,
# fatal error
raise exception.ViolinBackendErrExists()
elif "Error: 0x90010089" in response['msg']:
# lun export failed - lun is still being created as copy,
# fatal error
raise exception.ViolinBackendErr(message=response['msg'])
else:
# assume any other error is fatal
raise exception.ViolinBackendErr(message=response['msg'])
def _get_volume_type_extra_spec(self, volume, spec_key):
"""Parse data stored in a volume_type's extra_specs table.
:param volume: volume object containing volume_type to query
:param spec_key: the metadata key to search for
:returns: string value associated with spec_key
"""
spec_value = None
ctxt = context.get_admin_context()
typeid = volume['volume_type_id']
if typeid:
volume_type = volume_types.get_volume_type(ctxt, typeid)
volume_specs = volume_type.get('extra_specs')
for key, val in volume_specs.iteritems():
# Strip the prefix "capabilities"
if ':' in key:
scope = key.split(':')
key = scope[1]
if key == spec_key:
spec_value = val
break
return spec_value
def _get_violin_extra_spec(self, volume, spec_key):
"""Parse volume_type's extra_specs table for a violin-specific key.
:param volume: volume object containing volume_type to query
:param spec_key: the metadata key to search for
:returns: string value associated with spec_key
"""
spec_value = None
ctxt = context.get_admin_context()
typeid = volume['volume_type_id']
if typeid:
volume_type = volume_types.get_volume_type(ctxt, typeid)
volume_specs = volume_type.get('extra_specs')
for key, val in volume_specs.iteritems():
# Strip the prefix "violin"
if ':' in key:
scope = key.split(':')
key = scope[1]
if scope[0] == "violin" and key == spec_key:
spec_value = val
break
return spec_value

View File

@ -0,0 +1,384 @@
# Copyright 2015 Violin Memory, Inc.
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
"""
Violin 7000 Series All-Flash Array Volume Driver
Provides fibre channel specific LUN services for V7000 series flash
arrays.
This driver requires Concerto v7.0.0 or newer software on the array.
You will need to install the Violin Memory REST client library:
sudo pip install vmemclient
Set the following in the cinder.conf file to enable the VMEM V7000
Fibre Channel Driver along with the required flags:
volume_driver=cinder.volume.drivers.violin.v7000_fcp.V7000FCDriver
NOTE: this driver file requires the use of synchronization points for
certain types of backend operations, and as a result may not work
properly in an active-active HA configuration. See OpenStack Cinder
driver documentation for more information.
"""
from oslo_log import log as logging
from cinder import exception
from cinder.i18n import _, _LE, _LI
from cinder import utils
from cinder.volume import driver
from cinder.volume.drivers.san import san
from cinder.volume.drivers.violin import v7000_common
from cinder.zonemanager import utils as fczm_utils
import socket
LOG = logging.getLogger(__name__)
class V7000FCPDriver(driver.FibreChannelDriver):
"""Executes commands relating to fibre channel based Violin Memory arrays.
Version history:
1.0 - Initial driver
"""
VERSION = '1.0'
def __init__(self, *args, **kwargs):
super(V7000FCPDriver, self).__init__(*args, **kwargs)
self.gateway_fc_wwns = []
self.stats = {}
self.configuration.append_config_values(v7000_common.violin_opts)
self.configuration.append_config_values(san.san_opts)
self.common = v7000_common.V7000Common(self.configuration)
self.lookup_service = fczm_utils.create_lookup_service()
LOG.info(_LI("Initialized driver %(name)s version: %(vers)s"),
{'name': self.__class__.__name__, 'vers': self.VERSION})
def do_setup(self, context):
"""Any initialization the driver does while starting."""
super(V7000FCPDriver, self).do_setup(context)
self.common.do_setup(context)
self.gateway_fc_wwns = self._get_active_fc_targets()
# Register the client with the storage array
fc_version = self.VERSION + "-FCP"
self.common.vmem_mg.utility.set_managed_by_openstack_version(
fc_version)
def check_for_setup_error(self):
"""Returns an error if prerequisites aren't met."""
self.common.check_for_setup_error()
if len(self.gateway_fc_wwns) == 0:
raise exception.ViolinInvalidBackendConfig(
reason=_('No FCP targets found'))
def create_volume(self, volume):
"""Creates a volume."""
self.common._create_lun(volume)
def create_volume_from_snapshot(self, volume, snapshot):
"""Creates a volume from a snapshot."""
self.common._create_volume_from_snapshot(snapshot, volume)
def create_cloned_volume(self, volume, src_vref):
"""Creates a clone of the specified volume."""
self.common._create_lun_from_lun(src_vref, volume)
def delete_volume(self, volume):
"""Deletes a volume."""
self.common._delete_lun(volume)
def extend_volume(self, volume, new_size):
"""Extend an existing volume's size."""
self.common._extend_lun(volume, new_size)
def create_snapshot(self, snapshot):
"""Creates a snapshot."""
self.common._create_lun_snapshot(snapshot)
def delete_snapshot(self, snapshot):
"""Deletes a snapshot."""
self.common._delete_lun_snapshot(snapshot)
def ensure_export(self, context, volume):
"""Synchronously checks and re-exports volumes at cinder start time."""
pass
def create_export(self, context, volume):
"""Exports the volume."""
pass
def remove_export(self, context, volume):
"""Removes an export for a logical volume."""
pass
@fczm_utils.AddFCZone
def initialize_connection(self, volume, connector):
"""Allow connection to connector and return connection info."""
LOG.debug("Initialize_connection: initiator - %(initiator)s host - "
"%(host)s wwpns - %(wwpns)s",
{'initiator': connector['initiator'],
'host': connector['host'],
'wwpns': connector['wwpns']})
self.common.vmem_mg.client.create_client(
name=connector['host'], proto='FC', fc_wwns=connector['wwpns'])
lun_id = self._export_lun(volume, connector)
target_wwns, init_targ_map = self._build_initiator_target_map(
connector)
properties = {}
properties['target_discovered'] = True
properties['target_wwn'] = target_wwns
properties['target_lun'] = lun_id
properties['access_mode'] = 'rw'
properties['initiator_target_map'] = init_targ_map
LOG.debug("Return FC data for zone addition: %(properties)s.",
{'properties': properties})
return {'driver_volume_type': 'fibre_channel', 'data': properties}
@fczm_utils.RemoveFCZone
def terminate_connection(self, volume, connector, **kwargs):
"""Terminates the connection (target<-->initiator)."""
self._unexport_lun(volume, connector)
properties = {}
if not self._is_initiator_connected_to_array(connector):
target_wwns, init_targ_map = self._build_initiator_target_map(
connector)
properties['target_wwn'] = target_wwns
properties['initiator_target_map'] = init_targ_map
LOG.debug("Return FC data for zone deletion: %(properties)s.",
{'properties': properties})
return {'driver_volume_type': 'fibre_channel', 'data': properties}
def get_volume_stats(self, refresh=False):
"""Get volume stats.
If 'refresh' is True, update the stats first.
"""
if refresh or not self.stats:
self._update_volume_stats()
return self.stats
@utils.synchronized('vmem-export')
def _export_lun(self, volume, connector=None):
"""Generates the export configuration for the given volume.
:param volume: volume object provided by the Manager
:param connector: connector object provided by the Manager
:returns: the LUN ID assigned by the backend
"""
lun_id = ''
v = self.common.vmem_mg
if not connector:
raise exception.ViolinInvalidBackendConfig(
reason=_('No initiators found, cannot proceed'))
LOG.debug("Exporting lun %(vol_id)s - initiator wwpns %(i_wwpns)s "
"- target wwpns %(t_wwpns)s.",
{'vol_id': volume['id'], 'i_wwpns': connector['wwpns'],
't_wwpns': self.gateway_fc_wwns})
try:
lun_id = self.common._send_cmd_and_verify(
v.lun.assign_lun_to_client,
self._is_lun_id_ready,
"Assign SAN client successfully",
[volume['id'], connector['host'],
"ReadWrite"],
[volume['id'], connector['host']])
except exception.ViolinBackendErr:
LOG.exception(_LE("Backend returned err for lun export."))
raise
except Exception:
raise exception.ViolinInvalidBackendConfig(
reason=_('LUN export failed!'))
lun_id = self._get_lun_id(volume['id'], connector['host'])
LOG.info(_LI("Exported lun %(vol_id)s on lun_id %(lun_id)s."),
{'vol_id': volume['id'], 'lun_id': lun_id})
return lun_id
@utils.synchronized('vmem-export')
def _unexport_lun(self, volume, connector=None):
"""Removes the export configuration for the given volume.
:param volume: volume object provided by the Manager
"""
v = self.common.vmem_mg
LOG.info(_LI("Unexporting lun %s."), volume['id'])
try:
self.common._send_cmd(v.lun.unassign_client_lun,
"Unassign SAN client successfully",
volume['id'], connector['host'], True)
except exception.ViolinBackendErr:
LOG.exception(_LE("Backend returned err for lun export."))
raise
except Exception:
LOG.exception(_LE("LUN unexport failed!"))
raise
def _update_volume_stats(self):
"""Gathers array stats and converts them to GB values."""
data = {}
total_gb = 0
free_gb = 0
v = self.common.vmem_mg.basic
array_name_triple = socket.gethostbyaddr(self.configuration.san_ip)
array_name = array_name_triple[0]
phy_devices = v.get("/batch/physicalresource/physicaldevice")
all_devices = [x for x in phy_devices['data']['physical_devices']]
for x in all_devices:
if socket.getfqdn(x['owner']) == array_name:
total_gb += x['size_mb'] / 1024
free_gb += x['availsize_mb'] / 1024
backend_name = self.configuration.volume_backend_name
data['volume_backend_name'] = backend_name or self.__class__.__name__
data['vendor_name'] = 'Violin Memory, Inc.'
data['driver_version'] = self.VERSION
data['storage_protocol'] = 'fibre_channel'
data['reserved_percentage'] = 0
data['QoS_support'] = False
data['total_capacity_gb'] = total_gb
data['free_capacity_gb'] = free_gb
for i in data:
LOG.debug("stat update: %(name)s=%(data)s",
{'name': i, 'data': data[i]})
self.stats = data
def _get_active_fc_targets(self):
"""Get a list of gateway WWNs that can be used as FCP targets.
:param mg_conn: active XG connection to one of the gateways
:returns: list of WWNs in openstack format
"""
v = self.common.vmem_mg
active_gw_fcp_wwns = []
fc_info = v.adapter.get_fc_info()
for x in fc_info.itervalues():
active_gw_fcp_wwns.append(x[0])
return active_gw_fcp_wwns
def _get_lun_id(self, volume_name, client_name):
"""Get the lun ID for an exported volume.
If the lun is successfully assigned (exported) to a client, the
client info has the lun_id.
:param volume_name: name of volume to query for lun ID
:param client_name: name of client associated with the volume
:returns: integer value of lun ID
"""
v = self.common.vmem_mg
lun_id = -1
client_info = v.client.get_client_info(client_name)
for x in client_info['FibreChannelDevices']:
if volume_name == x['name']:
lun_id = x['lun']
break
return int(lun_id)
def _is_lun_id_ready(self, volume_name, client_name):
"""Get the lun ID for an exported volume.
If the lun is successfully assigned (exported) to a client, the
client info has the lun_id.
:param volume_name: name of volume to query for lun ID
:param client_name: name of client associated with the volume
:returns: Returns True if lun is ready, False otherwise
"""
lun_id = -1
lun_id = self._get_lun_id(volume_name, client_name)
if lun_id != -1:
return True
else:
return False
def _build_initiator_target_map(self, connector):
"""Build the target_wwns and the initiator target map."""
target_wwns = []
init_targ_map = {}
if self.lookup_service:
dev_map = self.lookup_service.get_device_mapping_from_network(
connector['wwpns'], self.gateway_fc_wwns)
for fabric_name in dev_map:
fabric = dev_map[fabric_name]
target_wwns += fabric['target_port_wwn_list']
for initiator in fabric['initiator_port_wwn_list']:
if initiator not in init_targ_map:
init_targ_map[initiator] = []
init_targ_map[initiator] += fabric['target_port_wwn_list']
init_targ_map[initiator] = list(
set(init_targ_map[initiator]))
target_wwns = list(set(target_wwns))
else:
initiator_wwns = connector['wwpns']
target_wwns = self.gateway_fc_wwns
for initiator in initiator_wwns:
init_targ_map[initiator] = target_wwns
return target_wwns, init_targ_map
def _is_initiator_connected_to_array(self, connector):
"""Check if any initiator wwns still have active sessions."""
v = self.common.vmem_mg
client = v.client.get_client_info(connector['host'])
if len(client['FibreChannelDevices']):
# each entry in the FibreChannelDevices array is a dict
# describing an active lun assignment
return True
return False