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
'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'
]
--- /dev/null
+# 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)
--- /dev/null
+# 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))
--- /dev/null
+# 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
--- /dev/null
+# 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