From 6e4cd718f8445fc8ad14ee155dd2e5c61a6fdf17 Mon Sep 17 00:00:00 2001 From: Ryan Lucio Date: Tue, 9 Jun 2015 13:17:53 -0700 Subject: [PATCH] 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 --- cinder/tests/unit/fake_vmem_client.py | 20 + cinder/tests/unit/test_v7000_common.py | 777 +++++++++++++++++ cinder/tests/unit/test_v7000_fcp.py | 563 ++++++++++++ cinder/volume/drivers/violin/v7000_common.py | 854 +++++++++++++++++++ cinder/volume/drivers/violin/v7000_fcp.py | 384 +++++++++ 5 files changed, 2598 insertions(+) create mode 100644 cinder/tests/unit/test_v7000_common.py create mode 100644 cinder/tests/unit/test_v7000_fcp.py create mode 100644 cinder/volume/drivers/violin/v7000_common.py create mode 100644 cinder/volume/drivers/violin/v7000_fcp.py diff --git a/cinder/tests/unit/fake_vmem_client.py b/cinder/tests/unit/fake_vmem_client.py index 25255a3c7..7b09c93fe 100644 --- a/cinder/tests/unit/fake_vmem_client.py +++ b/cinder/tests/unit/fake_vmem_client.py @@ -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' ] diff --git a/cinder/tests/unit/test_v7000_common.py b/cinder/tests/unit/test_v7000_common.py new file mode 100644 index 000000000..5894b6450 --- /dev/null +++ b/cinder/tests/unit/test_v7000_common.py @@ -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) diff --git a/cinder/tests/unit/test_v7000_fcp.py b/cinder/tests/unit/test_v7000_fcp.py new file mode 100644 index 000000000..4f12eea8a --- /dev/null +++ b/cinder/tests/unit/test_v7000_fcp.py @@ -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)) diff --git a/cinder/volume/drivers/violin/v7000_common.py b/cinder/volume/drivers/violin/v7000_common.py new file mode 100644 index 000000000..fbc01e671 --- /dev/null +++ b/cinder/volume/drivers/violin/v7000_common.py @@ -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 diff --git a/cinder/volume/drivers/violin/v7000_fcp.py b/cinder/volume/drivers/violin/v7000_fcp.py new file mode 100644 index 000000000..30d7d2c34 --- /dev/null +++ b/cinder/volume/drivers/violin/v7000_fcp.py @@ -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