From 4a679cc7f1cd8ed93eef30b35e7dd1ebe043ae8d Mon Sep 17 00:00:00 2001 From: Ryan Lucio Date: Sat, 6 Dec 2014 03:33:27 -0800 Subject: [PATCH] Fibrechannel and iSCSI for Violin Memory 6000 Series Arrays This patch adds both Fibrechannel and iSCSI driver support for Violin Memory 6000 Series All-Flash Arrays. Certification results posted at: https://bugs.launchpad.net/cinder/+bug/1399911 Change-Id: I9f62fe79e892cff01abdb02dbd95be8e432f8ab7 Implements: blueprint violinmemory-v6000-storage-drivers --- cinder/exception.py | 21 + cinder/tests/fake_vmem_xgtools_client.py | 46 ++ cinder/tests/test_v6000_common.py | 562 +++++++++++++++ cinder/tests/test_v6000_fcp.py | 585 +++++++++++++++ cinder/tests/test_v6000_iscsi.py | 718 +++++++++++++++++++ cinder/volume/drivers/violin/__init__.py | 0 cinder/volume/drivers/violin/v6000_common.py | 616 ++++++++++++++++ cinder/volume/drivers/violin/v6000_fcp.py | 522 ++++++++++++++ cinder/volume/drivers/violin/v6000_iscsi.py | 598 +++++++++++++++ 9 files changed, 3668 insertions(+) create mode 100644 cinder/tests/fake_vmem_xgtools_client.py create mode 100644 cinder/tests/test_v6000_common.py create mode 100644 cinder/tests/test_v6000_fcp.py create mode 100644 cinder/tests/test_v6000_iscsi.py create mode 100644 cinder/volume/drivers/violin/__init__.py create mode 100644 cinder/volume/drivers/violin/v6000_common.py create mode 100644 cinder/volume/drivers/violin/v6000_fcp.py create mode 100644 cinder/volume/drivers/violin/v6000_iscsi.py diff --git a/cinder/exception.py b/cinder/exception.py index 7c746cf9388..3373752d7d4 100755 --- a/cinder/exception.py +++ b/cinder/exception.py @@ -912,3 +912,24 @@ class ISCSITargetAttachFailed(CinderException): # X-IO driver exception. class XIODriverException(VolumeDriverException): message = _("X-IO Volume Driver exception!") + + +# Violin Memory drivers +class ViolinInvalidBackendConfig(CinderException): + message = _("Volume backend config is invalid: %(reason)s") + + +class ViolinRequestRetryTimeout(CinderException): + message = _("Backend service retry timeout hit: %(timeout)s sec") + + +class ViolinBackendErr(CinderException): + message = _("Backend reports: %(message)s") + + +class ViolinBackendErrExists(CinderException): + message = _("Backend reports: item already exists") + + +class ViolinBackendErrNotFound(CinderException): + message = _("Backend reports: item not found") diff --git a/cinder/tests/fake_vmem_xgtools_client.py b/cinder/tests/fake_vmem_xgtools_client.py new file mode 100644 index 00000000000..35316509917 --- /dev/null +++ b/cinder/tests/fake_vmem_xgtools_client.py @@ -0,0 +1,46 @@ +# Copyright 2014 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. + +""" +Fake VMEM XG-Tools client for testing drivers. Inspired by +cinder/tests/fake_hp_3par_client.py. +""" + +import sys + +import mock + +vmemclient = mock.Mock() +vmemclient.__version__ = "unknown" + +sys.modules['vxg'] = vmemclient + +mock_client_conf = [ + 'basic', + 'basic.login', + 'basic.get_node_values', + 'basic.save_config', + 'lun', + 'lun.export_lun', + 'lun.unexport_lun', + 'snapshot', + 'snapshot.export_lun_snapshot', + 'snapshot.unexport_lun_snapshot', + 'iscsi', + 'iscsi.bind_ip_to_target', + 'iscsi.create_iscsi_target', + 'iscsi.delete_iscsi_target', + 'igroup', +] diff --git a/cinder/tests/test_v6000_common.py b/cinder/tests/test_v6000_common.py new file mode 100644 index 00000000000..69822399c4c --- /dev/null +++ b/cinder/tests/test_v6000_common.py @@ -0,0 +1,562 @@ +# Copyright 2014 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 6000 Series All-Flash Array Common Driver +""" + +import mock + +from cinder import exception +from cinder import test +from cinder.tests import fake_vmem_xgtools_client as vxg +from cinder.volume import configuration as conf +from cinder.volume.drivers.violin import v6000_common + +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", + "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": "irrelevant", + "volume_type": None, + "volume_type_id": None, +} +INITIATOR_IQN = "iqn.1111-22.org.debian:11:222" +CONNECTOR = { + "initiator": INITIATOR_IQN, + "host": "irrelevant" +} + + +class V6000CommonTestCase(test.TestCase): + """Test cases for VMEM V6000 driver common class.""" + def setUp(self): + super(V6000CommonTestCase, self).setUp() + self.conf = self.setup_configuration() + self.driver = v6000_common.V6000Common(self.conf) + self.driver.container = 'myContainer' + self.driver.device_id = 'ata-VIOLIN_MEMORY_ARRAY_23109R00000022' + self.stats = {} + + def tearDown(self): + super(V6000CommonTestCase, self).tearDown() + + def setup_configuration(self): + config = mock.Mock(spec=conf.Configuration) + config.volume_backend_name = 'v6000_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.request_timeout = 300 + config.container = 'myContainer' + return config + + @mock.patch('vxg.open') + def setup_mock_client(self, _m_client, m_conf=None): + """Create a fake backend communication factory. + + The xg-tools creates a VShare connection object (for V6000 + devices) and returns it for use on a call to vxg.open(). + """ + # configure the vshare object mock with defaults + _m_vshare = mock.Mock(name='VShare', + version='1.1.1', + spec=vxg.mock_client_conf) + + # if m_conf, clobber the defaults with it + if m_conf: + _m_vshare.configure_mock(**m_conf) + + # set calls to vxg.open() to return this mocked vshare object + _m_client.return_value = _m_vshare + + return _m_client + + def setup_mock_vshare(self, m_conf=None): + """Create a fake VShare communication object.""" + _m_vshare = mock.Mock(name='VShare', + version='1.1.1', + spec=vxg.mock_client_conf) + + if m_conf: + _m_vshare.configure_mock(**m_conf) + + return _m_vshare + + def test_check_for_setup_error(self): + """No setup errors are found.""" + bn1 = ("/vshare/state/local/container/%s/threshold/usedspace" + "/threshold_hard_val" % self.driver.container) + bn2 = ("/vshare/state/local/container/%s/threshold/provision" + "/threshold_hard_val" % self.driver.container) + bn_thresholds = {bn1: 0, bn2: 100} + + conf = { + 'basic.get_node_values.return_value': bn_thresholds, + } + self.driver.vip = self.setup_mock_vshare(m_conf=conf) + 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.vip.version) + self.driver.vip.basic.get_node_values.assert_called_with( + [bn1, bn2]) + self.assertEqual(None, result) + + def test_check_for_setup_error_no_container(self): + """No container was configured.""" + self.driver.vip = self.setup_mock_vshare() + self.driver.container = '' + self.assertRaises(exception.ViolinInvalidBackendConfig, + self.driver.check_for_setup_error) + + def test_check_for_setup_error_invalid_usedspace_threshold(self): + """The array's usedspace threshold was altered (not supported).""" + bn1 = ("/vshare/state/local/container/%s/threshold/usedspace" + "/threshold_hard_val" % self.driver.container) + bn2 = ("/vshare/state/local/container/%s/threshold/provision" + "/threshold_hard_val" % self.driver.container) + bn_thresholds = {bn1: 99, bn2: 100} + + conf = { + 'basic.get_node_values.return_value': bn_thresholds, + } + self.driver.vip = self.setup_mock_vshare(m_conf=conf) + self.driver._is_supported_vmos_version = mock.Mock(return_value=True) + + self.assertRaises(exception.ViolinInvalidBackendConfig, + self.driver.check_for_setup_error) + + def test_check_for_setup_error_invalid_provisionedspace_threshold(self): + """The array's provisioned threshold was altered (not supported).""" + bn1 = ("/vshare/state/local/container/%s/threshold/usedspace" + "/threshold_hard_val" % self.driver.container) + bn2 = ("/vshare/state/local/container/%s/threshold/provision" + "/threshold_hard_val" % self.driver.container) + bn_thresholds = {bn1: 0, bn2: 99} + + conf = { + 'basic.get_node_values.return_value': bn_thresholds, + } + self.driver.vip = self.setup_mock_vshare(m_conf=conf) + self.driver._is_supported_vmos_version = mock.Mock(return_value=True) + + self.assertRaises(exception.ViolinInvalidBackendConfig, + self.driver.check_for_setup_error) + + def test_create_lun(self): + """Lun is successfully created.""" + response = {'code': 0, 'message': 'LUN create: success!'} + + conf = { + 'lun.create_lun.return_value': response, + } + self.driver.vip = self.setup_mock_vshare(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.vip.lun.create_lun, 'LUN create: success!', + self.driver.container, VOLUME['id'], VOLUME['size'], 1, "0", + "0", "w", 1, 512, False, False, None) + self.assertTrue(result is None) + + def test_create_lun_lun_already_exists(self): + """Array returns error that the lun already exists.""" + response = {'code': 14005, + 'message': 'LUN with name ... already exists'} + + conf = { + 'lun.create_lun.return_value': response, + } + self.driver.vip = self.setup_mock_client(m_conf=conf) + self.driver._send_cmd = mock.Mock( + side_effect=exception.ViolinBackendErrExists( + response['message'])) + + self.assertTrue(self.driver._create_lun(VOLUME) is None) + + def test_create_lun_create_fails_with_exception(self): + """Array returns a out of space error.""" + response = {'code': 512, 'message': 'Not enough space available'} + failure = exception.ViolinBackendErr + + conf = { + 'lun.create_lun.return_value': response, + } + self.driver.vip = self.setup_mock_vshare(m_conf=conf) + self.driver._send_cmd = mock.Mock( + side_effect=failure(response['message'])) + + self.assertRaises(failure, self.driver._create_lun, VOLUME) + + def test_delete_lun(self): + """Lun is deleted successfully.""" + response = {'code': 0, 'message': 'lun deletion started'} + success_msgs = ['lun deletion started', ''] + + conf = { + 'lun.delete_lun.return_value': response, + } + self.driver.vip = self.setup_mock_vshare(m_conf=conf) + self.driver._send_cmd = mock.Mock(return_value=response) + + result = self.driver._delete_lun(VOLUME) + + self.driver._send_cmd.assert_called_with( + self.driver.vip.lun.bulk_delete_luns, + success_msgs, self.driver.container, VOLUME['id']) + + self.assertTrue(result is None) + + def test_delete_lun_empty_response_message(self): + """Array bug where delete action returns no message.""" + response = {'code': 0, 'message': ''} + + conf = { + 'lun.delete_lun.return_value': response, + } + self.driver.vip = self.setup_mock_vshare(m_conf=conf) + self.driver._send_cmd = mock.Mock(return_value=response) + + self.assertTrue(self.driver._delete_lun(VOLUME) is None) + + def test_delete_lun_lun_already_deleted(self): + """Array fails to delete a lun that doesn't exist.""" + response = {'code': 14005, 'message': 'LUN ... does not exist.'} + + conf = { + 'lun.delete_lun.return_value': response, + } + self.driver.vip = self.setup_mock_vshare(m_conf=conf) + self.driver._send_cmd = mock.Mock( + side_effect=exception.ViolinBackendErrNotFound( + response['message'])) + + self.assertTrue(self.driver._delete_lun(VOLUME) is None) + + def test_delete_lun_delete_fails_with_exception(self): + """Array returns a generic error.""" + response = {'code': 14000, 'message': 'Generic error'} + failure = exception.ViolinBackendErr + conf = { + 'lun.delete_lun.return_value': response + } + self.driver.vip = self.setup_mock_vshare(m_conf=conf) + self.driver._send_cmd = mock.Mock( + side_effect=failure(response['message'])) + + self.assertRaises(failure, self.driver._delete_lun, VOLUME) + + def test_extend_lun(self): + """Volume extend completes successfully.""" + new_volume_size = 10 + response = {'code': 0, 'message': 'Success '} + + conf = { + 'lun.resize_lun.return_value': response, + } + self.driver.vip = self.setup_mock_vshare(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.vip.lun.resize_lun, + 'Success', self.driver.container, + VOLUME['id'], new_volume_size) + self.assertTrue(result is None) + + def test_extend_lun_new_size_is_too_small(self): + """Volume extend fails when new size would shrink the volume.""" + new_volume_size = 0 + response = {'code': 14036, 'message': 'Failure'} + + conf = { + 'lun.resize_lun.return_value': response, + } + self.driver.vip = self.setup_mock_vshare(m_conf=conf) + self.driver._send_cmd = mock.Mock( + side_effect=exception.ViolinBackendErr(message='fail')) + + self.assertRaises(exception.ViolinBackendErr, + self.driver._extend_lun, VOLUME, new_volume_size) + + def test_create_lun_snapshot(self): + """Snapshot creation completes successfully.""" + response = {'code': 0, 'message': 'success'} + success_msg = 'Snapshot create: success!' + + conf = { + 'snapshot.create_lun_snapshot.return_value': response + } + self.driver.vip = self.setup_mock_vshare(m_conf=conf) + self.driver._send_cmd = mock.Mock(return_value=response) + + result = self.driver._create_lun_snapshot(SNAPSHOT) + + self.driver._send_cmd.assert_called_with( + self.driver.vip.snapshot.create_lun_snapshot, success_msg, + self.driver.container, SNAPSHOT['volume_id'], SNAPSHOT['id']) + self.assertTrue(result is None) + + def test_delete_lun_snapshot(self): + """Snapshot deletion completes successfully.""" + response = {'code': 0, 'message': 'success'} + success_msg = 'Snapshot delete: success!' + + conf = { + 'snapshot.delete_lun_snapshot.return_value': response, + } + self.driver.vip = self.setup_mock_vshare(m_conf=conf) + self.driver._send_cmd = mock.Mock(return_value=response) + + result = self.driver._delete_lun_snapshot(SNAPSHOT) + + self.driver._send_cmd.assert_called_with( + self.driver.vip.snapshot.delete_lun_snapshot, success_msg, + self.driver.container, SNAPSHOT['volume_id'], SNAPSHOT['id']) + self.assertTrue(result is None) + + def test_get_lun_id(self): + bn = "/vshare/config/export/container/%s/lun/%s/target/**" \ + % (self.conf.container, VOLUME['id']) + response = {("/vshare/config/export/container/%s/lun" + "/%s/target/hba-a1/initiator/openstack/lun_id" + % (self.conf.container, VOLUME['id'])): 1} + + conf = { + 'basic.get_node_values.return_value': response, + } + self.driver.vip = self.setup_mock_vshare(m_conf=conf) + + result = self.driver._get_lun_id(VOLUME['id']) + + self.driver.vip.basic.get_node_values.assert_called_with(bn) + self.assertEqual(1, result) + + def test_get_lun_id_with_no_lun_config(self): + response = {} + + conf = { + 'basic.get_node_values.return_value': response, + } + self.driver.vip = self.setup_mock_vshare(m_conf=conf) + + self.assertRaises(exception.ViolinBackendErrNotFound, + self.driver._get_lun_id, VOLUME['id']) + + def test_get_snapshot_id(self): + bn = ("/vshare/config/export/snapshot/container/%s/lun/%s/snap/%s" + "/target/**") % (self.conf.container, VOLUME['id'], + SNAPSHOT['id']) + response = {("/vshare/config/export/snapshot/container/%s/lun" + "/%s/snap/%s/target/hba-a1/initiator/openstack/lun_id" + % (self.conf.container, VOLUME['id'], + SNAPSHOT['id'])): 1} + + conf = { + 'basic.get_node_values.return_value': response, + } + self.driver.vip = self.setup_mock_vshare(m_conf=conf) + + result = self.driver._get_snapshot_id(VOLUME['id'], SNAPSHOT['id']) + + self.driver.vip.basic.get_node_values.assert_called_with(bn) + self.assertEqual(1, result) + + def test_get_snapshot_id_with_no_lun_config(self): + response = {} + + conf = { + 'basic.get_node_values.return_value': response, + } + self.driver.vip = self.setup_mock_vshare(m_conf=conf) + + self.assertRaises(exception.ViolinBackendErrNotFound, + self.driver._get_snapshot_id, + SNAPSHOT['volume_id'], SNAPSHOT['id']) + + def test_send_cmd(self): + """Command callback completes successfully.""" + success_msg = 'success' + request_args = ['arg1', 'arg2', 'arg3'] + response = {'code': 0, 'message': 'success'} + + request_func = mock.Mock(return_value=response) + self.driver._fatal_error_code = mock.Mock(return_value=None) + + result = self.driver._send_cmd(request_func, success_msg, request_args) + + self.driver._fatal_error_code.assert_called_with(response) + self.assertEqual(response, result) + + def test_send_cmd_request_timed_out(self): + """The callback retry timeout hits immediately.""" + success_msg = 'success' + request_args = ['arg1', 'arg2', 'arg3'] + self.conf.request_timeout = 0 + + request_func = mock.Mock() + + self.assertRaises(exception.ViolinRequestRetryTimeout, + 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 = {'code': 0, 'message': None} + response2 = {'code': 0, 'message': 'success'} + + request_func = mock.Mock(side_effect=[response1, response2]) + self.driver._fatal_error_code = mock.Mock(return_value=None) + + self.assertEqual(response2, self.driver._send_cmd + (request_func, success_msg, request_args)) + + def test_send_cmd_response_has_fatal_error(self): + """The callback response contains a fatal error code.""" + success_msg = 'success' + request_args = ['arg1', 'arg2', 'arg3'] + response = {'code': 14000, 'message': 'try again later.'} + failure = exception.ViolinBackendErr + + request_func = mock.Mock(return_value=response) + self.driver._fatal_error_code = mock.Mock( + side_effect=failure(message='fail')) + self.assertRaises(failure, self.driver._send_cmd, + request_func, success_msg, request_args) + + def test_get_igroup(self): + """The igroup is verified and already exists.""" + bn = '/vshare/config/igroup/%s' % CONNECTOR['host'] + response = {bn: CONNECTOR['host']} + + conf = { + 'basic.get_node_values.return_value': response, + } + self.driver.vip = self.setup_mock_vshare(m_conf=conf) + + result = self.driver._get_igroup(VOLUME, CONNECTOR) + + self.driver.vip.basic.get_node_values.assert_called_with(bn) + self.assertEqual(CONNECTOR['host'], result) + + def test_get_igroup_with_new_name(self): + """The igroup is verified but must be created on the backend.""" + response = {} + + conf = { + 'basic.get_node_values.return_value': response, + } + self.driver.vip = self.setup_mock_vshare(m_conf=conf) + + self.assertEqual(CONNECTOR['host'], + self.driver._get_igroup(VOLUME, CONNECTOR)) + + def test_wait_for_export_config(self): + """Queries to cluster nodes verify export config.""" + bn = "/vshare/config/export/container/myContainer/lun/%s" \ + % VOLUME['id'] + response = {'/vshare/config/export/container/myContainer/lun/vol-01': + VOLUME['id']} + + conf = { + 'basic.get_node_values.return_value': response, + } + self.driver.mga = self.setup_mock_vshare(m_conf=conf) + self.driver.mgb = self.setup_mock_vshare(m_conf=conf) + + result = self.driver._wait_for_export_config(VOLUME['id'], state=True) + + self.driver.mga.basic.get_node_values.assert_called_with(bn) + self.driver.mgb.basic.get_node_values.assert_called_with(bn) + self.assertTrue(result) + + def test_wait_for_export_config_with_no_config(self): + """Queries to cluster nodes verify *no* export config.""" + response = {} + + conf = { + 'basic.get_node_values.return_value': response, + } + self.driver.mga = self.setup_mock_vshare(m_conf=conf) + self.driver.mgb = self.setup_mock_vshare(m_conf=conf) + + self.assertTrue(self.driver._wait_for_export_config( + VOLUME['id'], state=False)) + + def test_is_supported_vmos_version(self): + """Currently supported VMOS version.""" + version = 'V6.3.1' + self.assertTrue(self.driver._is_supported_vmos_version(version)) + + def test_is_supported_vmos_version_supported_future_version(self): + """Potential future supported VMOS version.""" + version = 'V6.3.7' + self.assertTrue(self.driver._is_supported_vmos_version(version)) + + def test_is_supported_vmos_version_unsupported_past_version(self): + """Currently unsupported VMOS version.""" + version = 'G5.5.2' + self.assertFalse(self.driver._is_supported_vmos_version(version)) + + def test_is_supported_vmos_version_unsupported_future_version(self): + """Future incompatible VMOS version.""" + version = 'V7.0.0' + self.assertFalse(self.driver._is_supported_vmos_version(version)) + + def test_fatal_error_code(self): + """Return an exception for a valid fatal error code.""" + response = {'code': 14000, 'message': 'fail city'} + self.assertRaises(exception.ViolinBackendErr, + self.driver._fatal_error_code, + response) + + def test_fatal_error_code_non_fatal_error(self): + """Returns no exception for a non-fatal error code.""" + response = {'code': 1024, 'message': 'try again!'} + self.assertEqual(None, self.driver._fatal_error_code(response)) diff --git a/cinder/tests/test_v6000_fcp.py b/cinder/tests/test_v6000_fcp.py new file mode 100644 index 00000000000..34d50920407 --- /dev/null +++ b/cinder/tests/test_v6000_fcp.py @@ -0,0 +1,585 @@ +# Copyright 2014 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 6000 Series All-Flash Array Fibrechannel Driver +""" + +import mock +from oslo.utils import units + +from cinder import context +from cinder.db.sqlalchemy import models +from cinder import exception +from cinder import test +from cinder.tests import fake_vmem_xgtools_client as vxg +from cinder.volume import configuration as conf +from cinder.volume.drivers.violin import v6000_common +from cinder.volume.drivers.violin import v6000_fcp + +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", + "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": "irrelevant", + "volume_type": None, + "volume_type_id": None, +} +INITIATOR_IQN = "iqn.1111-22.org.debian:11:222" +CONNECTOR = { + "initiator": INITIATOR_IQN, + "host": "irrelevant", + 'wwpns': [u'50014380186b3f65', u'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]] +} + + +class V6000FCPDriverTestCase(test.TestCase): + """Test cases for VMEM FCP driver.""" + def setUp(self): + super(V6000FCPDriverTestCase, self).setUp() + self.conf = self.setup_configuration() + self.driver = v6000_fcp.V6000FCDriver(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(V6000FCPDriverTestCase, self).tearDown() + + def setup_configuration(self): + config = mock.Mock(spec=conf.Configuration) + config.volume_backend_name = 'v6000_fcp' + 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.request_timeout = 300 + config.container = 'myContainer' + return config + + def setup_mock_vshare(self, m_conf=None): + """Create a fake VShare communication object.""" + _m_vshare = mock.Mock(name='VShare', + version='1.1.1', + spec=vxg.mock_client_conf) + + if m_conf: + _m_vshare.configure_mock(**m_conf) + + return _m_vshare + + @mock.patch.object(v6000_common.V6000Common, '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.assertTrue(result is None) + + @mock.patch.object(v6000_common.V6000Common, '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 = [] + self.assertRaises(exception.ViolinInvalidBackendConfig, + 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.assertTrue(result is None) + + 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.assertTrue(result is None) + + def test_create_snapshot(self): + """Snapshot created successfully.""" + 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.assertTrue(result is None) + + def test_delete_snapshot(self): + """Snapshot deleted successfully.""" + 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.assertTrue(result is None) + + @mock.patch.object(context, 'get_admin_context') + def test_create_volume_from_snapshot(self, m_context_func): + """Volume created from a snapshot successfully.""" + m_context_func.return_value = None + self.driver.common._create_lun = mock.Mock() + self.driver.copy_volume_data = mock.Mock() + + result = self.driver.create_volume_from_snapshot(VOLUME, SNAPSHOT) + + m_context_func.assert_called_with() + self.driver.common._create_lun.assert_called_with(VOLUME) + self.driver.copy_volume_data.assert_called_with(None, SNAPSHOT, VOLUME) + self.assertTrue(result is None) + + @mock.patch.object(context, 'get_admin_context') + def test_create_cloned_volume(self, m_context_func): + """Volume clone created successfully.""" + m_context_func.return_value = None + self.driver.common._create_lun = mock.Mock() + self.driver.copy_volume_data = mock.Mock() + + result = self.driver.create_cloned_volume(VOLUME, SRC_VOL) + + m_context_func.assert_called_with() + self.driver.common._create_lun.assert_called_with(VOLUME) + self.driver.copy_volume_data.assert_called_with(None, SRC_VOL, VOLUME) + self.assertTrue(result is None) + + def test_initialize_connection(self): + lun_id = 1 + igroup = None + target_wwns = self.driver.gateway_fc_wwns + init_targ_map = {} + volume = mock.Mock(spec=models.Volume) + + self.driver.common.vip = self.setup_mock_vshare() + 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._export_lun.assert_called_with(volume, CONNECTOR, igroup) + self.driver.common.vip.basic.save_config.assert_called_with() + self.driver._build_initiator_target_map.assert_called_with( + CONNECTOR) + self.assertEqual("fibre_channel", props['driver_volume_type']) + self.assertEqual(True, props['data']['target_discovered']) + self.assertEqual(target_wwns, props['data']['target_wwn']) + self.assertEqual(lun_id, props['data']['target_lun']) + self.assertEqual(init_targ_map, props['data']['initiator_target_map']) + + def test_initialize_connection_with_snapshot_object(self): + lun_id = 1 + igroup = None + target_wwns = self.driver.gateway_fc_wwns + init_targ_map = {} + snapshot = mock.Mock(spec=models.Snapshot) + + self.driver.common.vip = self.setup_mock_vshare() + self.driver._export_snapshot = 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(snapshot, CONNECTOR) + + self.driver._export_snapshot.assert_called_with( + snapshot, CONNECTOR, igroup) + self.driver.common.vip.basic.save_config.assert_called_with() + self.driver._build_initiator_target_map.assert_called_with( + CONNECTOR) + self.assertEqual("fibre_channel", props['driver_volume_type']) + self.assertEqual(True, props['data']['target_discovered']) + self.assertEqual(target_wwns, props['data']['target_wwn']) + self.assertEqual(lun_id, props['data']['target_lun']) + self.assertEqual(init_targ_map, props['data']['initiator_target_map']) + + def test_terminate_connection(self): + target_wwns = self.driver.gateway_fc_wwns + init_targ_map = {} + volume = mock.Mock(spec=models.Volume) + + self.driver.common.vip = self.setup_mock_vshare() + 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) + self.driver.common.vip.basic.save_config.assert_called_with() + 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_terminate_connection_snapshot_object(self): + target_wwns = self.driver.gateway_fc_wwns + init_targ_map = {} + snapshot = mock.Mock(spec=models.Snapshot) + + self.driver.common.vip = self.setup_mock_vshare() + self.driver._unexport_snapshot = 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(snapshot, 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_get_volume_stats(self): + self.driver._update_stats = mock.Mock() + self.driver._update_stats() + + result = self.driver.get_volume_stats(True) + + self.driver._update_stats.assert_called_with() + self.assertEqual(self.driver.stats, result) + + def test_export_lun(self): + lun_id = '1' + igroup = 'test-igroup-1' + response = {'code': 0, 'message': ''} + + self.driver.common.vip = self.setup_mock_vshare() + self.driver.common._send_cmd_and_verify = mock.Mock( + return_value=response) + self.driver.common._get_lun_id = mock.Mock(return_value=lun_id) + + result = self.driver._export_lun(VOLUME, CONNECTOR, igroup) + + self.driver.common._send_cmd_and_verify.assert_called_with( + self.driver.common.vip.lun.export_lun, + self.driver.common._wait_for_export_config, '', + [self.driver.common.container, VOLUME['id'], 'all', + igroup, 'auto'], [VOLUME['id'], 'state=True']) + self.driver.common._get_lun_id.assert_called_with(VOLUME['id']) + self.assertEqual(lun_id, result) + + def test_export_lun_fails_with_exception(self): + lun_id = '1' + igroup = 'test-igroup-1' + response = {'code': 14000, 'message': 'Generic error'} + failure = exception.ViolinBackendErr + + self.driver.common.vip = self.setup_mock_vshare() + self.driver.common._send_cmd_and_verify = mock.Mock( + side_effect=failure(response['message'])) + self.driver.common._get_lun_id = mock.Mock(return_value=lun_id) + + self.assertRaises(failure, self.driver._export_lun, + VOLUME, CONNECTOR, igroup) + + def test_unexport_lun(self): + response = {'code': 0, 'message': ''} + + self.driver.common.vip = self.setup_mock_vshare() + self.driver.common._send_cmd_and_verify = mock.Mock( + return_value=response) + + result = self.driver._unexport_lun(VOLUME) + + self.driver.common._send_cmd_and_verify.assert_called_with( + self.driver.common.vip.lun.unexport_lun, + self.driver.common._wait_for_export_config, '', + [self.driver.common.container, VOLUME['id'], 'all', 'all', 'auto'], + [VOLUME['id'], 'state=False']) + self.assertTrue(result is None) + + def test_unexport_lun_fails_with_exception(self): + response = {'code': 14000, 'message': 'Generic error'} + failure = exception.ViolinBackendErr + + self.driver.common.vip = self.setup_mock_vshare() + self.driver.common._send_cmd_and_verify = mock.Mock( + side_effect=failure(response['message'])) + + self.assertRaises(failure, self.driver._unexport_lun, VOLUME) + + def test_export_snapshot(self): + lun_id = '1' + igroup = 'test-igroup-1' + response = {'code': 0, 'message': ''} + + self.driver.common.vip = self.setup_mock_vshare() + self.driver.common._send_cmd = mock.Mock(return_value=response) + self.driver.common._wait_for_export_config = mock.Mock() + self.driver.common._get_snapshot_id = mock.Mock(return_value=lun_id) + + result = self.driver._export_snapshot(SNAPSHOT, CONNECTOR, igroup) + + self.driver.common._send_cmd.assert_called_with( + self.driver.common.vip.snapshot.export_lun_snapshot, '', + self.driver.common.container, SNAPSHOT['volume_id'], + SNAPSHOT['id'], igroup, 'all', 'auto') + self.driver.common._wait_for_export_config.assert_called_with( + SNAPSHOT['volume_id'], SNAPSHOT['id'], state=True) + self.driver.common._get_snapshot_id.assert_called_once_with( + SNAPSHOT['volume_id'], SNAPSHOT['id']) + self.assertEqual(lun_id, result) + + def test_unexport_snapshot(self): + response = {'code': 0, 'message': ''} + + self.driver.common.vip = self.setup_mock_vshare() + self.driver.common._send_cmd = mock.Mock(return_value=response) + self.driver.common._wait_for_export_config = mock.Mock() + + result = self.driver._unexport_snapshot(SNAPSHOT) + + self.driver.common._send_cmd.assert_called_with( + self.driver.common.vip.snapshot.unexport_lun_snapshot, '', + self.driver.common.container, SNAPSHOT['volume_id'], + SNAPSHOT['id'], 'all', 'all', 'auto', False) + self.driver.common._wait_for_export_config.assert_called_with( + SNAPSHOT['volume_id'], SNAPSHOT['id'], state=False) + self.assertTrue(result is None) + + def test_add_igroup_member(self): + igroup = 'test-group-1' + response = {'code': 0, 'message': 'success'} + wwpns = ['wwn.50:01:43:80:18:6b:3f:65', 'wwn.50:01:43:80:18:6b:3f:67'] + + conf = { + 'igroup.add_initiators.return_value': response, + } + self.driver.common.vip = self.setup_mock_vshare(m_conf=conf) + + self.driver._convert_wwns_openstack_to_vmem = mock.Mock( + return_value=wwpns) + + result = self.driver._add_igroup_member(CONNECTOR, igroup) + + self.driver._convert_wwns_openstack_to_vmem.assert_called_with( + CONNECTOR['wwpns']) + self.driver.common.vip.igroup.add_initiators.assert_called_with( + igroup, wwpns) + self.assertTrue(result is None) + + def test_build_initiator_target_map(self): + """Successfully build a map when zoning is enabled.""" + expected_targ_wwns = FC_TARGET_WWPNS + expected_init_targ_map = FC_INITIATOR_TARGET_MAP + + self.driver.lookup_service = mock.Mock() + self.driver.lookup_service.get_device_mapping_from_network.\ + return_value = FC_FABRIC_MAP + + (targ_wwns, init_targ_map) = \ + self.driver._build_initiator_target_map(CONNECTOR) + + 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)) + self.assertEqual(expected_init_targ_map, 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.""" + converted_wwpns = ['50:01:43:80:18:6b:3f:65', + '50:01:43:80:18:6b:3f:67'] + prefix = "/vshare/config/export/container" + bn = "%s/%s/lun/**" % (prefix, self.driver.common.container) + resp_binding0 = "%s/%s/lun/%s/target/hba-a1/initiator/%s" \ + % (prefix, self.driver.common.container, VOLUME['id'], + converted_wwpns[0]) + resp_binding1 = "%s/%s/lun/%s/target/hba-a1/initiator/%s" \ + % (prefix, self.driver.common.container, VOLUME['id'], + converted_wwpns[1]) + response = { + resp_binding0: converted_wwpns[0], + resp_binding1: converted_wwpns[1] + } + + conf = { + 'basic.get_node_values.return_value': response, + } + self.driver.common.vip = self.setup_mock_vshare(m_conf=conf) + self.driver._convert_wwns_openstack_to_vmem = mock.Mock( + return_value=converted_wwpns) + + self.assertTrue(self.driver._is_initiator_connected_to_array( + CONNECTOR)) + self.driver.common.vip.basic.get_node_values.assert_called_with(bn) + + def test_is_initiator_connected_to_array_empty_response(self): + """Successfully finds no initiators with remaining active sessions.""" + converted_wwpns = ['50:01:43:80:18:6b:3f:65', + '50:01:43:80:18:6b:3f:67'] + response = {} + + conf = { + 'basic.get_node_values.return_value': response, + } + self.driver.common.vip = self.setup_mock_vshare(m_conf=conf) + self.driver._convert_wwns_openstack_to_vmem = mock.Mock( + return_value=converted_wwpns) + + self.assertFalse(self.driver._is_initiator_connected_to_array( + CONNECTOR)) + + def test_update_stats(self): + backend_name = self.conf.volume_backend_name + vendor_name = "Violin Memory, Inc." + tot_bytes = 100 * units.Gi + free_bytes = 50 * units.Gi + bn0 = '/cluster/state/master_id' + bn1 = "/vshare/state/global/1/container/myContainer/total_bytes" + bn2 = "/vshare/state/global/1/container/myContainer/free_bytes" + response1 = {bn0: '1'} + response2 = {bn1: tot_bytes, bn2: free_bytes} + + conf = { + 'basic.get_node_values.side_effect': [response1, response2], + } + self.driver.common.vip = self.setup_mock_vshare(m_conf=conf) + + result = self.driver._update_stats() + + calls = [mock.call(bn0), mock.call([bn1, bn2])] + self.driver.common.vip.basic.get_node_values.assert_has_calls(calls) + self.assertEqual(100, self.driver.stats['total_capacity_gb']) + self.assertEqual(50, 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.assertTrue(result is None) + + def test_update_stats_fails_data_query(self): + backend_name = self.conf.volume_backend_name + vendor_name = "Violin Memory, Inc." + bn0 = '/cluster/state/master_id' + response1 = {bn0: '1'} + response2 = {} + + conf = { + 'basic.get_node_values.side_effect': [response1, response2], + } + self.driver.common.vip = self.setup_mock_vshare(m_conf=conf) + + self.assertTrue(self.driver._update_stats() is None) + self.assertEqual(0, self.driver.stats['total_capacity_gb']) + self.assertEqual(0, 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']) + + def test_get_active_fc_targets(self): + bn0 = '/vshare/state/global/*' + response0 = {'/vshare/state/global/1': 1, + '/vshare/state/global/2': 2} + bn1 = '/vshare/state/global/1/target/fc/**' + response1 = {'/vshare/state/global/1/target/fc/hba-a1/wwn': + 'wwn.21:00:00:24:ff:45:fb:22'} + bn2 = '/vshare/state/global/2/target/fc/**' + response2 = {'/vshare/state/global/2/target/fc/hba-a1/wwn': + 'wwn.21:00:00:24:ff:45:e2:30'} + wwpns = ['21000024ff45fb22', '21000024ff45e230'] + + conf = { + 'basic.get_node_values.side_effect': + [response0, response1, response2], + } + self.driver.common.vip = self.setup_mock_vshare(m_conf=conf) + + result = self.driver._get_active_fc_targets() + + calls = [mock.call(bn0), mock.call(bn1), mock.call(bn2)] + self.driver.common.vip.basic.get_node_values.assert_has_calls( + calls, any_order=True) + self.assertEqual(wwpns, result) + + def test_convert_wwns_openstack_to_vmem(self): + vmem_wwns = ['wwn.50:01:43:80:18:6b:3f:65'] + openstack_wwns = ['50014380186b3f65'] + result = self.driver._convert_wwns_openstack_to_vmem(openstack_wwns) + self.assertEqual(vmem_wwns, result) + + def test_convert_wwns_vmem_to_openstack(self): + vmem_wwns = ['wwn.50:01:43:80:18:6b:3f:65'] + openstack_wwns = ['50014380186b3f65'] + result = self.driver._convert_wwns_vmem_to_openstack(vmem_wwns) + self.assertEqual(openstack_wwns, result) diff --git a/cinder/tests/test_v6000_iscsi.py b/cinder/tests/test_v6000_iscsi.py new file mode 100644 index 00000000000..2c9f8e750b3 --- /dev/null +++ b/cinder/tests/test_v6000_iscsi.py @@ -0,0 +1,718 @@ +# Copyright 2014 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 6000 Series All-Flash Array iSCSI driver +""" + +import mock +from oslo.utils import units + +from cinder import context +from cinder.db.sqlalchemy import models +from cinder import exception +from cinder import test +from cinder.tests import fake_vmem_xgtools_client as vxg +from cinder.volume import configuration as conf +from cinder.volume.drivers.violin import v6000_common +from cinder.volume.drivers.violin import v6000_iscsi + +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", + "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": "irrelevant", + "volume_type": None, + "volume_type_id": None, +} +INITIATOR_IQN = "iqn.1111-22.org.debian:11:222" +CONNECTOR = { + "initiator": INITIATOR_IQN, + "host": "irrelevant" +} + + +class V6000ISCSIDriverTestCase(test.TestCase): + """Test cases for VMEM iSCSI driver.""" + def setUp(self): + super(V6000ISCSIDriverTestCase, self).setUp() + self.conf = self.setup_configuration() + self.driver = v6000_iscsi.V6000ISCSIDriver(configuration=self.conf) + self.driver.common.container = 'myContainer' + self.driver.device_id = 'ata-VIOLIN_MEMORY_ARRAY_23109R00000022' + self.driver.gateway_iscsi_ip_addresses_mga = '1.2.3.4' + self.driver.gateway_iscsi_ip_addresses_mgb = '1.2.3.4' + self.driver.array_info = [{"node": 'hostname_mga', + "addr": '1.2.3.4', + "conn": self.driver.common.mga}, + {"node": 'hostname_mgb', + "addr": '1.2.3.4', + "conn": self.driver.common.mgb}] + self.stats = {} + self.driver.set_initialized() + + def tearDown(self): + super(V6000ISCSIDriverTestCase, self).tearDown() + + def setup_configuration(self): + config = mock.Mock(spec=conf.Configuration) + config.volume_backend_name = 'v6000_iscsi' + 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.request_timeout = 300 + config.container = 'myContainer' + config.iscsi_port = 3260 + config.iscsi_target_prefix = 'iqn.2004-02.com.vmem:' + return config + + def setup_mock_vshare(self, m_conf=None): + """Create a fake VShare communication object.""" + _m_vshare = mock.Mock(name='VShare', + version='1.1.1', + spec=vxg.mock_client_conf) + + if m_conf: + _m_vshare.configure_mock(**m_conf) + + return _m_vshare + + @mock.patch.object(v6000_common.V6000Common, 'check_for_setup_error') + def test_check_for_setup_error(self, m_setup_func): + bn = "/vshare/config/iscsi/enable" + response = {bn: True} + + conf = { + 'basic.get_node_values.return_value': response, + } + self.driver.common.vip = self.setup_mock_vshare(m_conf=conf) + + result = self.driver.check_for_setup_error() + + m_setup_func.assert_called_with() + self.driver.common.vip.basic.get_node_values.assert_called_with(bn) + self.assertTrue(result is None) + + @mock.patch.object(v6000_common.V6000Common, 'check_for_setup_error') + def test_check_for_setup_error_iscsi_is_disabled(self, m_setup_func): + bn = "/vshare/config/iscsi/enable" + response = {bn: False} + + conf = { + 'basic.get_node_values.return_value': response, + } + self.driver.common.vip = self.setup_mock_vshare(m_conf=conf) + + self.assertRaises(exception.ViolinInvalidBackendConfig, + self.driver.check_for_setup_error) + + @mock.patch.object(v6000_common.V6000Common, 'check_for_setup_error') + def test_check_for_setup_error_no_iscsi_ips_for_mga(self, m_setup_func): + bn = "/vshare/config/iscsi/enable" + response = {bn: True} + self.driver.gateway_iscsi_ip_addresses_mga = '' + + conf = { + 'basic.get_node_values.return_value': response, + } + self.driver.common.vip = self.setup_mock_vshare(m_conf=conf) + + self.assertRaises(exception.ViolinInvalidBackendConfig, + self.driver.check_for_setup_error) + + @mock.patch.object(v6000_common.V6000Common, 'check_for_setup_error') + def test_check_for_setup_error_no_iscsi_ips_for_mgb(self, m_setup_func): + bn = "/vshare/config/iscsi/enable" + response = {bn: True} + self.driver.gateway_iscsi_ip_addresses_mgb = '' + + conf = { + 'basic.get_node_values.return_value': response, + } + self.driver.common.vip = self.setup_mock_vshare(m_conf=conf) + + self.assertRaises(exception.ViolinInvalidBackendConfig, + 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.assertTrue(result is None) + + 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.assertTrue(result is None) + + def test_create_snapshot(self): + """Snapshot created successfully.""" + 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.assertTrue(result is None) + + def test_delete_snapshot(self): + """Snapshot deleted successfully.""" + 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.assertTrue(result is None) + + @mock.patch.object(context, 'get_admin_context') + def test_create_volume_from_snapshot(self, m_context_func): + """Volume created from a snapshot successfully.""" + m_context_func.return_value = None + self.driver.common._create_lun = mock.Mock() + self.driver.copy_volume_data = mock.Mock() + + result = self.driver.create_volume_from_snapshot(VOLUME, SNAPSHOT) + + m_context_func.assert_called_with() + self.driver.common._create_lun.assert_called_with(VOLUME) + self.driver.copy_volume_data.assert_called_with(None, SNAPSHOT, VOLUME) + self.assertTrue(result is None) + + @mock.patch.object(context, 'get_admin_context') + def test_create_cloned_volume(self, m_context_func): + """Volume clone created successfully.""" + m_context_func.return_value = None + self.driver.common._create_lun = mock.Mock() + self.driver.copy_volume_data = mock.Mock() + + result = self.driver.create_cloned_volume(VOLUME, SRC_VOL) + + m_context_func.assert_called_with() + self.driver.common._create_lun.assert_called_with(VOLUME) + self.driver.copy_volume_data.assert_called_with(None, SRC_VOL, VOLUME) + self.assertTrue(result is None) + + def test_initialize_connection(self): + lun_id = 1 + igroup = None + tgt = self.driver.array_info[0] + iqn = "%s%s:%s" % (self.conf.iscsi_target_prefix, + tgt['node'], VOLUME['id']) + volume = mock.MagicMock(spec=models.Volume) + + def getitem(name): + return VOLUME[name] + + volume.__getitem__.side_effect = getitem + + self.driver.common.vip = self.setup_mock_vshare() + self.driver._get_short_name = mock.Mock(return_value=VOLUME['id']) + self.driver._create_iscsi_target = mock.Mock(return_value=tgt) + self.driver._export_lun = mock.Mock(return_value=lun_id) + + props = self.driver.initialize_connection(volume, CONNECTOR) + + self.driver._get_short_name.assert_called_with(volume['id']) + self.driver._create_iscsi_target.assert_called_with(volume) + self.driver._export_lun.assert_called_with(volume, CONNECTOR, igroup) + self.driver.common.vip.basic.save_config.assert_called_with() + self.assertEqual("1.2.3.4:3260", props['data']['target_portal']) + self.assertEqual(iqn, props['data']['target_iqn']) + self.assertEqual(lun_id, props['data']['target_lun']) + self.assertEqual(volume['id'], props['data']['volume_id']) + + def test_initialize_connection_with_snapshot_object(self): + lun_id = 1 + igroup = None + tgt = self.driver.array_info[0] + iqn = "%s%s:%s" % (self.conf.iscsi_target_prefix, + tgt['node'], SNAPSHOT['id']) + snapshot = mock.MagicMock(spec=models.Snapshot) + + def getitem(name): + return SNAPSHOT[name] + + snapshot.__getitem__.side_effect = getitem + + self.driver.common.vip = self.setup_mock_vshare() + self.driver._get_short_name = mock.Mock(return_value=SNAPSHOT['id']) + self.driver._create_iscsi_target = mock.Mock(return_value=tgt) + self.driver._export_snapshot = mock.Mock(return_value=lun_id) + + props = self.driver.initialize_connection(snapshot, CONNECTOR) + + self.driver._get_short_name.assert_called_with(SNAPSHOT['id']) + self.driver._create_iscsi_target.assert_called_with(snapshot) + self.driver._export_snapshot.assert_called_with(snapshot, CONNECTOR, + igroup) + self.driver.common.vip.basic.save_config.assert_called_with() + self.assertEqual("1.2.3.4:3260", props['data']['target_portal']) + self.assertEqual(iqn, props['data']['target_iqn']) + self.assertEqual(lun_id, props['data']['target_lun']) + self.assertEqual(SNAPSHOT['id'], props['data']['volume_id']) + + def test_initialize_connection_with_igroups_enabled(self): + self.conf.use_igroups = True + lun_id = 1 + igroup = 'test-igroup-1' + tgt = self.driver.array_info[0] + iqn = "%s%s:%s" % (self.conf.iscsi_target_prefix, + tgt['node'], VOLUME['id']) + volume = mock.MagicMock(spec=models.Volume) + + def getitem(name): + return VOLUME[name] + + volume.__getitem__.side_effect = getitem + + self.driver.common.vip = self.setup_mock_vshare() + self.driver.common._get_igroup = mock.Mock(return_value=igroup) + self.driver._add_igroup_member = mock.Mock() + self.driver._get_short_name = mock.Mock(return_value=VOLUME['id']) + self.driver._create_iscsi_target = mock.Mock(return_value=tgt) + self.driver._export_lun = mock.Mock(return_value=lun_id) + + props = self.driver.initialize_connection(volume, CONNECTOR) + + self.driver.common._get_igroup.assert_called_with(volume, CONNECTOR) + self.driver._add_igroup_member.assert_called_with(CONNECTOR, igroup) + self.driver._get_short_name.assert_called_with(volume['id']) + self.driver._create_iscsi_target.assert_called_with(volume) + self.driver._export_lun.assert_called_with(volume, CONNECTOR, igroup) + self.driver.common.vip.basic.save_config.assert_called_with() + self.assertEqual("1.2.3.4:3260", props['data']['target_portal']) + self.assertEqual(iqn, props['data']['target_iqn']) + self.assertEqual(lun_id, props['data']['target_lun']) + self.assertEqual(volume['id'], props['data']['volume_id']) + + def test_terminate_connection(self): + volume = mock.MagicMock(spec=models.Volume) + + self.driver.common.vip = self.setup_mock_vshare() + self.driver._unexport_lun = mock.Mock() + self.driver._delete_iscsi_target = mock.Mock() + + result = self.driver.terminate_connection(volume, CONNECTOR) + + self.driver._unexport_lun.assert_called_with(volume) + self.driver._delete_iscsi_target.assert_called_with(volume) + self.driver.common.vip.basic.save_config.assert_called_with() + self.assertTrue(result is None) + + def test_terminate_connection_with_snapshot_object(self): + snapshot = mock.MagicMock(spec=models.Snapshot) + + self.driver.common.vip = self.setup_mock_vshare() + self.driver._unexport_snapshot = mock.Mock() + self.driver._delete_iscsi_target = mock.Mock() + + result = self.driver.terminate_connection(snapshot, CONNECTOR) + + self.driver._unexport_snapshot.assert_called_with(snapshot) + self.driver._delete_iscsi_target.assert_called_with(snapshot) + self.driver.common.vip.basic.save_config.assert_called_with() + self.assertTrue(result is None) + + def test_get_volume_stats(self): + self.driver._update_stats = mock.Mock() + self.driver._update_stats() + + result = self.driver.get_volume_stats(True) + + self.driver._update_stats.assert_called_with() + self.assertEqual(self.driver.stats, result) + + def test_create_iscsi_target(self): + target_name = VOLUME['id'] + response = {'code': 0, 'message': 'success'} + + m_vshare = self.setup_mock_vshare() + + self.driver.common.vip = m_vshare + self.driver.common.mga = m_vshare + self.driver.common.mgb = m_vshare + self.driver._get_short_name = mock.Mock(return_value=VOLUME['id']) + self.driver.common._send_cmd_and_verify = mock.Mock( + return_value=response) + self.driver.common._send_cmd = mock.Mock(return_value=response) + + calls = [mock.call(self.driver.common.mga.iscsi.bind_ip_to_target, '', + VOLUME['id'], + self.driver.gateway_iscsi_ip_addresses_mga), + mock.call(self.driver.common.mgb.iscsi.bind_ip_to_target, '', + VOLUME['id'], + self.driver.gateway_iscsi_ip_addresses_mgb)] + + result = self.driver._create_iscsi_target(VOLUME) + + self.driver._get_short_name.assert_called_with(VOLUME['id']) + self.driver.common._send_cmd_and_verify.assert_called_with( + self.driver.common.vip.iscsi.create_iscsi_target, + self.driver._wait_for_targetstate, '', + [target_name], [target_name]) + self.driver.common._send_cmd.assert_has_calls(calls) + self.assertTrue(result in self.driver.array_info) + + def test_delete_iscsi_target(self): + response = {'code': 0, 'message': 'success'} + + self.driver.common.vip = self.setup_mock_vshare() + self.driver._get_short_name = mock.Mock(return_value=VOLUME['id']) + self.driver.common._send_cmd = mock.Mock(return_value=response) + + result = self.driver._delete_iscsi_target(VOLUME) + + self.driver._get_short_name.assert_called_with(VOLUME['id']) + self.driver.common._send_cmd( + self.driver.common.vip.iscsi.delete_iscsi_target, + '', VOLUME['id']) + self.assertTrue(result is None) + + def test_delete_iscsi_target_fails_with_exception(self): + response = {'code': 14000, 'message': 'Generic error'} + failure = exception.ViolinBackendErr + + self.driver.common.vip = self.setup_mock_vshare() + self.driver._get_short_name = mock.Mock(return_value=VOLUME['id']) + self.driver.common._send_cmd = mock.Mock( + side_effect=failure(response['message'])) + + self.assertRaises(failure, self.driver._delete_iscsi_target, VOLUME) + + def test_export_lun(self): + igroup = 'test-igroup-1' + lun_id = '1' + response = {'code': 0, 'message': ''} + + self.driver.common.vip = self.setup_mock_vshare() + self.driver._get_short_name = mock.Mock(return_value=VOLUME['id']) + self.driver.common._send_cmd_and_verify = mock.Mock( + return_value=response) + self.driver.common._get_lun_id = mock.Mock(return_value=lun_id) + + result = self.driver._export_lun(VOLUME, CONNECTOR, igroup) + + self.driver._get_short_name.assert_called_with(VOLUME['id']) + self.driver.common._send_cmd_and_verify.assert_called_with( + self.driver.common.vip.lun.export_lun, + self.driver.common._wait_for_export_config, '', + [self.driver.common.container, VOLUME['id'], VOLUME['id'], + igroup, 'auto'], [VOLUME['id'], 'state=True']) + self.driver.common._get_lun_id.assert_called_with(VOLUME['id']) + self.assertEqual(lun_id, result) + + def test_export_lun_fails_with_exception(self): + igroup = 'test-igroup-1' + lun_id = '1' + response = {'code': 14000, 'message': 'Generic error'} + failure = exception.ViolinBackendErr + + self.driver.common.vip = self.setup_mock_vshare() + self.driver._get_short_name = mock.Mock(return_value=VOLUME['id']) + self.driver.common._send_cmd_and_verify = mock.Mock( + side_effect=failure(response['message'])) + self.driver._get_lun_id = mock.Mock(return_value=lun_id) + + self.assertRaises(failure, self.driver._export_lun, + VOLUME, CONNECTOR, igroup) + + def test_unexport_lun(self): + response = {'code': 0, 'message': ''} + + self.driver.common.vip = self.setup_mock_vshare() + self.driver.common._send_cmd_and_verify = mock.Mock( + return_value=response) + + result = self.driver._unexport_lun(VOLUME) + + self.driver.common._send_cmd_and_verify.assert_called_with( + self.driver.common.vip.lun.unexport_lun, + self.driver.common._wait_for_export_config, '', + [self.driver.common.container, VOLUME['id'], 'all', 'all', 'auto'], + [VOLUME['id'], 'state=False']) + self.assertTrue(result is None) + + def test_unexport_lun_fails_with_exception(self): + response = {'code': 14000, 'message': 'Generic error'} + failure = exception.ViolinBackendErr + + self.driver.common.vip = self.setup_mock_vshare() + self.driver.common._send_cmd_and_verify = mock.Mock( + side_effect=failure(response['message'])) + + self.assertRaises(failure, self.driver._unexport_lun, VOLUME) + + def test_export_snapshot(self): + lun_id = '1' + igroup = 'test-igroup-1' + response = {'code': 0, 'message': ''} + + self.driver.common.vip = self.setup_mock_vshare() + self.driver._get_short_name = mock.Mock(return_value=SNAPSHOT['id']) + self.driver.common._send_cmd = mock.Mock(return_value=response) + self.driver.common._wait_for_export_config = mock.Mock() + self.driver.common._get_snapshot_id = mock.Mock(return_value=lun_id) + + result = self.driver._export_snapshot(SNAPSHOT, CONNECTOR, igroup) + + self.driver._get_short_name.assert_called_with(SNAPSHOT['id']) + self.driver.common._send_cmd.assert_called_with( + self.driver.common.vip.snapshot.export_lun_snapshot, '', + self.driver.common.container, SNAPSHOT['volume_id'], + SNAPSHOT['id'], igroup, SNAPSHOT['id'], 'auto') + self.driver.common._wait_for_export_config.assert_called_with( + SNAPSHOT['volume_id'], SNAPSHOT['id'], state=True) + self.driver.common._get_snapshot_id.assert_called_once_with( + SNAPSHOT['volume_id'], SNAPSHOT['id']) + + self.assertEqual(lun_id, result) + + def test_unexport_snapshot(self): + response = {'code': 0, 'message': ''} + + self.driver.common.vip = self.setup_mock_vshare() + self.driver.common._send_cmd = mock.Mock(return_value=response) + self.driver.common._wait_for_export_config = mock.Mock() + + result = self.driver._unexport_snapshot(SNAPSHOT) + + self.driver.common._send_cmd.assert_called_with( + self.driver.common.vip.snapshot.unexport_lun_snapshot, '', + self.driver.common.container, SNAPSHOT['volume_id'], + SNAPSHOT['id'], 'all', 'all', 'auto', False) + self.driver.common._wait_for_export_config.assert_called_with( + SNAPSHOT['volume_id'], SNAPSHOT['id'], state=False) + self.assertTrue(result is None) + + def test_add_igroup_member(self): + igroup = 'test-group-1' + response = {'code': 0, 'message': 'success'} + + conf = { + 'igroup.add_initiators.return_value': response, + } + self.driver.common.vip = self.setup_mock_vshare(m_conf=conf) + + result = self.driver._add_igroup_member(CONNECTOR, igroup) + + self.driver.common.vip.igroup.add_initiators.assert_called_with( + igroup, CONNECTOR['initiator']) + self.assertTrue(result is None) + + def test_update_stats(self): + backend_name = self.conf.volume_backend_name + vendor_name = "Violin Memory, Inc." + tot_bytes = 100 * units.Gi + free_bytes = 50 * units.Gi + bn0 = '/cluster/state/master_id' + bn1 = "/vshare/state/global/1/container/myContainer/total_bytes" + bn2 = "/vshare/state/global/1/container/myContainer/free_bytes" + response1 = {bn0: '1'} + response2 = {bn1: tot_bytes, bn2: free_bytes} + + conf = { + 'basic.get_node_values.side_effect': [response1, response2], + } + self.driver.common.vip = self.setup_mock_vshare(m_conf=conf) + + result = self.driver._update_stats() + + calls = [mock.call(bn0), mock.call([bn1, bn2])] + self.driver.common.vip.basic.get_node_values.assert_has_calls(calls) + self.assertEqual(100, self.driver.stats['total_capacity_gb']) + self.assertEqual(50, 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.assertTrue(result is None) + + def test_update_stats_fails_data_query(self): + backend_name = self.conf.volume_backend_name + vendor_name = "Violin Memory, Inc." + bn0 = '/cluster/state/master_id' + response1 = {bn0: '1'} + response2 = {} + + conf = { + 'basic.get_node_values.side_effect': [response1, response2], + } + self.driver.common.vip = self.setup_mock_vshare(m_conf=conf) + + self.assertTrue(self.driver._update_stats() is None) + self.assertEqual(0, self.driver.stats['total_capacity_gb']) + self.assertEqual(0, 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']) + + def testGetShortName_LongName(self): + long_name = "abcdefghijklmnopqrstuvwxyz1234567890" + short_name = "abcdefghijklmnopqrstuvwxyz123456" + self.assertEqual(short_name, self.driver._get_short_name(long_name)) + + def testGetShortName_ShortName(self): + long_name = "abcdef" + short_name = "abcdef" + self.assertEqual(short_name, self.driver._get_short_name(long_name)) + + def testGetShortName_EmptyName(self): + long_name = "" + short_name = "" + self.assertEqual(short_name, self.driver._get_short_name(long_name)) + + def test_get_active_iscsi_ips(self): + bn0 = "/net/interface/config/*" + bn1 = ["/net/interface/state/eth4/addr/ipv4/1/ip", + "/net/interface/state/eth4/flags/link_up"] + response1 = {"/net/interface/config/eth4": "eth4"} + response2 = {"/net/interface/state/eth4/addr/ipv4/1/ip": "1.1.1.1", + "/net/interface/state/eth4/flags/link_up": True} + + conf = { + 'basic.get_node_values.side_effect': [response1, response2], + } + self.driver.common.vip = self.setup_mock_vshare(m_conf=conf) + + results = self.driver._get_active_iscsi_ips(self.driver.common.vip) + + calls = [mock.call(bn0), mock.call(bn1)] + self.driver.common.vip.basic.get_node_values.assert_has_calls(calls) + self.assertEqual(1, len(results)) + self.assertEqual("1.1.1.1", results[0]) + + def test_get_active_iscsi_ips_with_invalid_interfaces(self): + response = {"/net/interface/config/lo": "lo", + "/net/interface/config/vlan10": "vlan10", + "/net/interface/config/eth1": "eth1", + "/net/interface/config/eth2": "eth2", + "/net/interface/config/eth3": "eth3"} + + conf = { + 'basic.get_node_values.return_value': response, + } + self.driver.common.vip = self.setup_mock_vshare(m_conf=conf) + + result = self.driver._get_active_iscsi_ips(self.driver.common.vip) + + self.assertEqual(0, len(result)) + + def test_get_active_iscsi_ips_with_no_interfaces(self): + response = {} + + conf = { + 'basic.get_node_values.return_value': response, + } + self.driver.common.vip = self.setup_mock_vshare(m_conf=conf) + + result = self.driver._get_active_iscsi_ips(self.driver.common.vip) + + self.assertEqual(0, len(result)) + + def test_get_hostname(self): + bn = '/system/hostname' + response = {bn: 'MYHOST'} + + conf = { + 'basic.get_node_values.return_value': response, + } + self.driver.common.vip = self.setup_mock_vshare(m_conf=conf) + + result = self.driver._get_hostname() + + self.driver.common.vip.basic.get_node_values.assert_called_with(bn) + self.assertEqual("MYHOST", result) + + def test_get_hostname_mga(self): + bn = '/system/hostname' + response = {bn: 'MYHOST'} + + conf = { + 'basic.get_node_values.return_value': response, + } + self.driver.common.vip = self.setup_mock_vshare(m_conf=conf) + self.driver.common.mga = self.setup_mock_vshare(m_conf=conf) + self.assertEqual("MYHOST", self.driver._get_hostname('mga')) + + def test_get_hostname_mgb(self): + response = {"/system/hostname": "MYHOST"} + bn = '/system/hostname' + response = {bn: 'MYHOST'} + + conf = { + 'basic.get_node_values.return_value': response, + } + self.driver.common.vip = self.setup_mock_vshare(m_conf=conf) + self.driver.common.mgb = self.setup_mock_vshare(m_conf=conf) + self.assertEqual("MYHOST", self.driver._get_hostname('mgb')) + + def test_get_hostname_query_fails(self): + response = {} + + conf = { + 'basic.get_node_values.return_value': response, + } + self.driver.common.vip = self.setup_mock_vshare(m_conf=conf) + + self.assertEqual(self.conf.san_ip, self.driver._get_hostname()) + + def test_wait_for_targetstate(self): + target = 'mytarget' + bn = "/vshare/config/iscsi/target/%s" % target + response = {bn: target} + + conf = { + 'basic.get_node_values.return_value': response, + } + self.driver.common.mga = self.setup_mock_vshare(m_conf=conf) + self.driver.common.mgb = self.setup_mock_vshare(m_conf=conf) + + result = self.driver._wait_for_targetstate(target) + + self.driver.common.mga.basic.get_node_values.assert_called_with(bn) + self.driver.common.mgb.basic.get_node_values.assert_called_with(bn) + self.assertTrue(result) diff --git a/cinder/volume/drivers/violin/__init__.py b/cinder/volume/drivers/violin/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/cinder/volume/drivers/violin/v6000_common.py b/cinder/volume/drivers/violin/v6000_common.py new file mode 100644 index 00000000000..6872659d12f --- /dev/null +++ b/cinder/volume/drivers/violin/v6000_common.py @@ -0,0 +1,616 @@ +# Copyright 2014 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 6000 Series All-Flash Array Common Driver for Openstack Cinder + +Provides common (ie., non-protocol specific) management functions for +V6000 series flash arrays. + +Backend array communication is handled via VMEM's python library +called 'xg-tools'. + +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 re +import time + +from oslo.config import cfg +from oslo.utils import importutils + +from cinder import exception +from cinder.i18n import _, _LE, _LW, _LI +from cinder.openstack.common import log as logging +from cinder.openstack.common import loopingcall +from cinder import utils + +LOG = logging.getLogger(__name__) + +vxg = importutils.try_import("vxg") +if vxg: + LOG.info(_LI("Running with xg-tools version: %s."), vxg.__version__) + +# version vmos versions V6.3.0.4 or newer +VMOS_SUPPORTED_VERSION_PATTERNS = ['V6.3.0.[4-9]', 'V6.3.[1-9].?[0-9]?'] + +violin_opts = [ + cfg.StrOpt('gateway_mga', + default=None, + help='IP address or hostname of mg-a'), + cfg.StrOpt('gateway_mgb', + default=None, + help='IP address or hostname of mg-b'), + cfg.BoolOpt('use_igroups', + default=False, + help='Use igroups to manage targets and initiators'), + cfg.IntOpt('request_timeout', + default=300, + help='Global backend request timeout, in seconds'), +] + +CONF = cfg.CONF +CONF.register_opts(violin_opts) + + +class V6000Common(object): + """Contains common code for the Violin V6000 drivers. + + Version history: + 1.0 - Initial driver + """ + + VERSION = '1.0' + + def __init__(self, config): + self.vip = None + self.mga = None + self.mgb = 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 option \'san_ip\' is not set')) + if not self.config.gateway_mga: + raise exception.InvalidInput( + reason=_('Gateway MG-A IP option \'gateway_mga\' is not set')) + if not self.config.gateway_mgb: + raise exception.InvalidInput( + reason=_('Gateway MG-B IP option \'gateway_mgb\' is not set')) + if self.config.request_timeout <= 0: + raise exception.InvalidInput( + reason=_('Global timeout option \'request_timeout\' must be ' + 'greater than 0')) + + self.vip = vxg.open(self.config.san_ip, self.config.san_login, + self.config.san_password, keepalive=True) + self.mga = vxg.open(self.config.gateway_mga, self.config.san_login, + self.config.san_password, keepalive=True) + self.mgb = vxg.open(self.config.gateway_mgb, self.config.san_login, + self.config.san_password, keepalive=True) + + ret_dict = self.vip.basic.get_node_values( + "/vshare/state/local/container/*") + if ret_dict: + self.container = ret_dict.items()[0][1] + + def check_for_setup_error(self): + """Returns an error if prerequisites aren't met.""" + + if len(self.container) == 0: + msg = _('container is missing') + raise exception.ViolinInvalidBackendConfig(reason=msg) + + if not self._is_supported_vmos_version(self.vip.version): + msg = _('VMOS version is not supported') + raise exception.ViolinInvalidBackendConfig(reason=msg) + + bn1 = ("/vshare/state/local/container/%s/threshold/usedspace" + "/threshold_hard_val" % self.container) + bn2 = ("/vshare/state/local/container/%s/threshold/provision" + "/threshold_hard_val" % self.container) + ret_dict = self.vip.basic.get_node_values([bn1, bn2]) + + for node in ret_dict: + # The infrastructure does not support space reclamation so + # ensure it is disabled. When used space exceeds the hard + # limit, snapshot space reclamation begins. Default is 0 + # => no space reclamation. + # + if node.endswith('/usedspace/threshold_hard_val'): + if ret_dict[node] != 0: + msg = _('space reclamation threshold is enabled but not ' + 'supported by Cinder infrastructure.') + raise exception.ViolinInvalidBackendConfig(reason=msg) + + # The infrastructure does not support overprovisioning so + # ensure it is disabled. When provisioned space exceeds + # the hard limit, further provisioning is stopped. + # Default is 100 => provisioned space equals usable space. + # + elif node.endswith('/provision/threshold_hard_val'): + if ret_dict[node] != 100: + msg = _('provisioned space threshold is not equal to ' + 'usable space.') + raise exception.ViolinInvalidBackendConfig(reason=msg) + + @utils.synchronized('vmem-lun') + def _create_lun(self, volume): + """Creates a new lun. + + The equivalent CLI command is "lun create container + name size " + + Arguments: + volume -- volume object provided by the Manager + """ + lun_type = '0' + + LOG.debug("Creating LUN %(name)s, %(size)s GB." % + {'name': volume['name'], 'size': volume['size']}) + + if self.config.san_thin_provision: + lun_type = '1' + + # using the defaults for fields: quantity, nozero, + # readonly, startnum, blksize, naca, alua, preferredport + # + try: + self._send_cmd(self.vip.lun.create_lun, + 'LUN create: success!', + self.container, volume['id'], + volume['size'], 1, '0', lun_type, 'w', + 1, 512, False, False, None) + + except exception.ViolinBackendErrExists: + LOG.debug("Lun %s already exists, continuing.", volume['id']) + + except Exception: + LOG.warn(_LW("Lun create for %s failed!"), volume['id']) + raise + + @utils.synchronized('vmem-lun') + def _delete_lun(self, volume): + """Deletes a lun. + + The equivalent CLI command is "no lun create container + name " + + Arguments: + volume -- volume object provided by the Manager + """ + success_msgs = ['lun deletion started', ''] + + LOG.debug("Deleting lun %s.", volume['id']) + + try: + self._send_cmd(self.vip.lun.bulk_delete_luns, + success_msgs, self.container, volume['id']) + + except exception.ViolinBackendErrNotFound: + LOG.debug("Lun %s already deleted, continuing.", volume['id']) + + except exception.ViolinBackendErrExists: + LOG.warn(_LW("Lun %s has dependent snapshots, skipping."), + volume['id']) + raise exception.VolumeIsBusy(volume_name=volume['id']) + + except Exception: + LOG.exception(_LE("Lun delete for %s failed!"), volume['id']) + raise + + @utils.synchronized('vmem-lun') + def _extend_lun(self, volume, new_size): + """Extend an existing volume's size. + + The equivalent CLI command is "lun resize container + name size " + + Arguments: + volume -- volume object provided by the Manager + new_size -- new (increased) size in GB to be applied + """ + LOG.debug("Extending lun %(id)s, from %(size)s to %(new_size)s GB." % + {'id': volume['id'], 'size': volume['size'], + 'new_size': new_size}) + + try: + self._send_cmd(self.vip.lun.resize_lun, 'Success', + self.container, volume['id'], new_size) + + except Exception: + LOG.exception(_LE("LUN extend for %s failed!"), volume['id']) + raise + + @utils.synchronized('vmem-snap') + def _create_lun_snapshot(self, snapshot): + """Creates a new snapshot for a lun. + + The equivalent CLI command is "snapshot create container + lun name " + + Arguments: + snapshot -- snapshot object provided by the Manager + """ + LOG.debug("Creating snapshot %s.", snapshot['id']) + + try: + self._send_cmd(self.vip.snapshot.create_lun_snapshot, + 'Snapshot create: success!', + self.container, snapshot['volume_id'], + snapshot['id']) + + except exception.ViolinBackendErrExists: + LOG.debug("Snapshot %s already exists, continuing.", + snapshot['id']) + + except Exception: + LOG.exception(_LE("LUN snapshot create for %s failed!"), + snapshot['id']) + raise + + @utils.synchronized('vmem-snap') + def _delete_lun_snapshot(self, snapshot): + """Deletes an existing snapshot for a lun. + + The equivalent CLI command is "no snapshot create container + lun name " + + Arguments: + snapshot -- snapshot object provided by the Manager + """ + LOG.debug("Deleting snapshot %s.", snapshot['id']) + + try: + self._send_cmd(self.vip.snapshot.delete_lun_snapshot, + 'Snapshot delete: success!', + self.container, snapshot['volume_id'], + snapshot['id']) + + except exception.ViolinBackendErrNotFound: + LOG.debug("Snapshot %s already deleted, continuing.", + snapshot['id']) + + except Exception: + LOG.exception(_LE("LUN snapshot delete for %s failed!"), + snapshot['id']) + raise + + def _get_lun_id(self, volume_name): + """Queries the gateway to find the lun id for the exported volume. + + Arguments: + volume_name -- LUN to query + + Returns: + LUN ID for the exported lun. + """ + lun_id = -1 + + prefix = "/vshare/config/export/container" + bn = "%s/%s/lun/%s/target/**" % (prefix, self.container, volume_name) + resp = self.vip.basic.get_node_values(bn) + + for node in resp: + if node.endswith('/lun_id'): + lun_id = resp[node] + break + + if lun_id == -1: + raise exception.ViolinBackendErrNotFound() + return lun_id + + def _get_snapshot_id(self, volume_name, snapshot_name): + """Queries the gateway to find the lun id for the exported snapshot. + + Arguments: + volume_name -- LUN to query + snapshot_name -- Exported snapshot associated with LUN + + Returns: + LUN ID for the exported lun + """ + lun_id = -1 + + prefix = "/vshare/config/export/snapshot/container" + bn = "%s/%s/lun/%s/snap/%s/target/**" \ + % (prefix, self.container, volume_name, snapshot_name) + resp = self.vip.basic.get_node_values(bn) + + for node in resp: + if node.endswith('/lun_id'): + lun_id = resp[node] + break + + if lun_id == -1: + raise exception.ViolinBackendErrNotFound() + return lun_id + + def _send_cmd(self, request_func, success_msgs, *args): + """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. 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). + + Arguments: + request_func -- XG api method to call + success_msgs -- Success messages expected from the backend + *args -- argument array to be passed to the 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.request_timeout: + raise exception.ViolinRequestRetryTimeout( + timeout=self.config.request_timeout) + + resp = request_func(*args) + + if not resp['message']: + # XG requests will return None for a message if no message + # string is passed in the raw response + resp['message'] = '' + + for msg in success_msgs: + if not resp['code'] and msg in resp['message']: + done = True + break + + self._fatal_error_code(resp) + + 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). + + Arguments: + request_func -- XG api method to call + verify_func -- function to call to verify request was + completed successfully (eg for export) + request_success_msg -- Success message expected from the backend + for the request_func + rargs -- argument array to be passed to the + request_func + vargs -- argument array to be passed to the + 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.request_timeout: + raise exception.ViolinRequestRetryTimeout( + timeout=self.config.request_timeout) + + if request_needed: + resp = request_func(*rargs) + if not resp['message']: + # XG requests will return None for a message if no message + # string is passed int the raw response + resp['message'] = '' + for msg in request_success_msgs: + if not resp['code'] and msg in resp['message']: + # XG request func was completed + request_needed = False + break + self._fatal_error_code(resp) + + elif verify_needed: + success = verify_func(*vargs) + if success: + # XG verify func was completed + verify_needed = False + else: + # try sending the request again + request_needed = True + + return resp + + def _get_igroup(self, volume, connector): + """Gets the igroup that should be used when configuring a volume. + + Arguments: + volume -- volume object used to determine the igroup name + + Returns: + igroup_name -- name of igroup (for configuring targets & + initiators) + """ + # Use the connector's primary hostname and use that as the + # name of the igroup. The name must follow syntax rules + # required by the array: "must contain only alphanumeric + # characters, dashes, and underscores. The first character + # must be alphanumeric". + # + igroup_name = re.sub(r'[\W]', '_', connector['host']) + + # verify that the igroup has been created on the backend, and + # if it doesn't exist, create it! + # + bn = "/vshare/config/igroup/%s" % igroup_name + resp = self.vip.basic.get_node_values(bn) + + if not len(resp): + self.vip.igroup.create_igroup(igroup_name) + + return igroup_name + + def _wait_for_export_config(self, volume_name, snapshot_name=None, + state=False): + """Polls backend to verify volume's export configuration. + + XG sets/queries following a request to create or delete a lun + export may fail on the backend if vshared is still processing + the export action (or times out). We can check whether it is + done by polling the export binding for a lun to ensure it is + created or deleted. + + This function will try to verify the creation or removal of + export state on both gateway nodes of the array every 5 + seconds. + + Arguments: + volume_name -- name of volume + snapshot_name -- name of volume's snapshot + state -- True to poll for existence, False for lack of + + Returns: + True if the export state was correctly added or removed + (depending on 'state' param) + """ + if not snapshot_name: + bn = "/vshare/config/export/container/%s/lun/%s" \ + % (self.container, volume_name) + else: + bn = "/vshare/config/export/snapshot/container/%s/lun/%s/snap/%s" \ + % (self.container, volume_name, snapshot_name) + + def _loop_func(state): + status = [False, False] + mg_conns = [self.mga, self.mgb] + + LOG.debug("Entering _wait_for_export_config loop: state=%s.", + state) + + for node_id in xrange(2): + resp = mg_conns[node_id].basic.get_node_values(bn) + if state and len(resp.keys()): + status[node_id] = True + elif (not state) and (not len(resp.keys())): + status[node_id] = True + + if status[0] and status[1]: + raise loopingcall.LoopingCallDone(retvalue=True) + + timer = loopingcall.FixedIntervalLoopingCall(_loop_func, state) + success = timer.start(interval=5).wait() + + return success + + def _is_supported_vmos_version(self, version_string): + """Check that the array s/w version is supported. """ + for pattern in VMOS_SUPPORTED_VERSION_PATTERNS: + if re.match(pattern, version_string): + LOG.info(_LI("Verified VMOS version %s is supported."), + version_string) + return True + return False + + def _fatal_error_code(self, response): + """Raise an exception for certain errors in a XG response. + + Error codes are extracted from vdmd_mgmt.c. + + Arguments: + response -- a response dict result from an XG request + """ + # known non-fatal response codes: + # 1024: 'lun deletion in progress, try again later' + # 14032: 'lc_err_lock_busy' + + if response['code'] == 14000: + # lc_generic_error + raise exception.ViolinBackendErr(message=response['message']) + elif response['code'] == 14002: + # lc_err_assertion_failed + raise exception.ViolinBackendErr(message=response['message']) + elif response['code'] == 14004: + # lc_err_not_found + raise exception.ViolinBackendErrNotFound() + elif response['code'] == 14005: + # lc_err_exists + raise exception.ViolinBackendErrExists() + elif response['code'] == 14008: + # lc_err_unexpected_arg + raise exception.ViolinBackendErr(message=response['message']) + elif response['code'] == 14014: + # lc_err_io_error + raise exception.ViolinBackendErr(message=response['message']) + elif response['code'] == 14016: + # lc_err_io_closed + raise exception.ViolinBackendErr(message=response['message']) + elif response['code'] == 14017: + # lc_err_io_timeout + raise exception.ViolinBackendErr(message=response['message']) + elif response['code'] == 14021: + # lc_err_unexpected_case + raise exception.ViolinBackendErr(message=response['message']) + elif response['code'] == 14025: + # lc_err_no_fs_space + raise exception.ViolinBackendErr(message=response['message']) + elif response['code'] == 14035: + # lc_err_range + raise exception.ViolinBackendErr(message=response['message']) + elif response['code'] == 14036: + # lc_err_invalid_param + raise exception.ViolinBackendErr(message=response['message']) + elif response['code'] == 14121: + # lc_err_cancelled_err + raise exception.ViolinBackendErr(message=response['message']) + elif response['code'] == 512: + # Not enough free space in container (vdmd bug) + raise exception.ViolinBackendErr(message=response['message']) + elif response['code'] == 1 and 'LUN ID conflict' \ + in response['message']: + # lun id conflict while attempting to export + raise exception.ViolinBackendErr(message=response['message']) diff --git a/cinder/volume/drivers/violin/v6000_fcp.py b/cinder/volume/drivers/violin/v6000_fcp.py new file mode 100644 index 00000000000..aeedf494fb6 --- /dev/null +++ b/cinder/volume/drivers/violin/v6000_fcp.py @@ -0,0 +1,522 @@ +# Copyright 2014 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 Fibre Channel Driver for Openstack Cinder + +Provides fibre channel specific LUN services for V6000 series flash +arrays. + +This driver requires VMOS v6.3.0.4 or newer software on the array. + +You will need to install the python xg-tools client: +sudo pip install xg-tools + +Set the following in the cinder.conf file to enable the VMEM V6000 +Fibre Channel Driver along with the required flags: + +volume_driver=cinder.volume.drivers.violin.v6000_fcp.V6000FCDriver + +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.utils import units + +from cinder import context +from cinder.db.sqlalchemy import models +from cinder import exception +from cinder.i18n import _, _LE, _LI, _LW +from cinder.openstack.common import log as logging +from cinder import utils +from cinder.volume import driver +from cinder.volume.drivers.san import san +from cinder.volume.drivers.violin import v6000_common +from cinder.zonemanager import utils as fczm_utils + +LOG = logging.getLogger(__name__) + + +class V6000FCDriver(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(V6000FCDriver, self).__init__(*args, **kwargs) + self.gateway_fc_wwns = [] + self.stats = {} + self.configuration.append_config_values(v6000_common.violin_opts) + self.configuration.append_config_values(san.san_opts) + self.common = v6000_common.V6000Common(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(V6000FCDriver, self).do_setup(context) + self.common.do_setup(context) + self.gateway_fc_wwns = self._get_active_fc_targets() + + 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 delete_volume(self, volume): + """Deletes a volume.""" + self.common._delete_lun(volume) + + def extend_volume(self, volume, new_size): + """Deletes a volume.""" + self.common._extend_lun(volume, new_size) + + def create_snapshot(self, snapshot): + """Creates a snapshot from an existing volume.""" + self.common._create_lun_snapshot(snapshot) + + def delete_snapshot(self, snapshot): + """Deletes a snapshot.""" + self.common._delete_lun_snapshot(snapshot) + + def create_volume_from_snapshot(self, volume, snapshot): + """Creates a volume from a snapshot.""" + ctxt = context.get_admin_context() + snapshot['size'] = snapshot['volume']['size'] + self.common._create_lun(volume) + self.copy_volume_data(ctxt, snapshot, volume) + + def create_cloned_volume(self, volume, src_vref): + """Creates a full clone of the specified volume.""" + ctxt = context.get_admin_context() + self.common._create_lun(volume) + self.copy_volume_data(ctxt, src_vref, volume) + + 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): + """Initializes the connection (target<-->initiator).""" + igroup = None + + if self.configuration.use_igroups: + # + # Most drivers don't use igroups, because there are a + # number of issues with multipathing and iscsi/fcp where + # lun devices either aren't cleaned up properly or are + # stale (from previous scans). + # + # If the customer really wants igroups for whatever + # reason, we create a new igroup for each host/hypervisor. + # Every lun that is exported to the particular + # hypervisor/host will be contained in this igroup. This + # should prevent other hosts from seeing luns they aren't + # using when they perform scans. + # + igroup = self.common._get_igroup(volume, connector) + self._add_igroup_member(connector, igroup) + + if isinstance(volume, models.Volume): + lun_id = self._export_lun(volume, connector, igroup) + else: + lun_id = self._export_snapshot(volume, connector, igroup) + self.common.vip.basic.save_config() + + 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['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, force=False, **kwargs): + """Terminates the connection (target<-->initiator).""" + + if isinstance(volume, models.Volume): + self._unexport_lun(volume) + else: + self._unexport_snapshot(volume) + + self.common.vip.basic.save_config() + + 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 or not self.stats: + self._update_stats() + return self.stats + + @utils.synchronized('vmem-export') + def _export_lun(self, volume, connector=None, igroup=None): + """Generates the export configuration for the given volume. + + The equivalent CLI command is "lun export container + name " + + Arguments: + volume -- volume object provided by the Manager + connector -- connector object provided by the Manager + igroup -- name of igroup to use for exporting + + Returns: + lun_id -- the LUN ID assigned by the backend + """ + lun_id = -1 + export_to = '' + v = self.common.vip + + if igroup: + export_to = igroup + elif connector: + export_to = self._convert_wwns_openstack_to_vmem( + connector['wwpns']) + else: + raise exception.Error(_("No initiators found, cannot proceed")) + + LOG.debug("Exporting lun %s." % volume['id']) + + try: + self.common._send_cmd_and_verify( + v.lun.export_lun, self.common._wait_for_export_config, '', + [self.common.container, volume['id'], 'all', export_to, + 'auto'], [volume['id'], 'state=True']) + + except Exception: + LOG.exception(_LE("LUN export for %s failed!"), volume['id']) + raise + + lun_id = self.common._get_lun_id(volume['id']) + + return lun_id + + @utils.synchronized('vmem-export') + def _unexport_lun(self, volume): + """Removes the export configuration for the given volume. + + The equivalent CLI command is "no lun export container + name " + + Arguments: + volume -- volume object provided by the Manager + """ + v = self.common.vip + + LOG.debug("Unexporting lun %s.", volume['id']) + + try: + self.common._send_cmd_and_verify( + v.lun.unexport_lun, self.common._wait_for_export_config, '', + [self.common.container, volume['id'], 'all', 'all', 'auto'], + [volume['id'], 'state=False']) + + except exception.ViolinBackendErrNotFound: + LOG.debug("Lun %s already unexported, continuing.", volume['id']) + + except Exception: + LOG.exception(_LE("LUN unexport for %s failed!"), volume['id']) + raise + + @utils.synchronized('vmem-export') + def _export_snapshot(self, snapshot, connector=None, igroup=None): + """Generates the export configuration for the given snapshot. + + The equivalent CLI command is "snapshot export container + PROD08 lun name " + + Arguments: + snapshot -- snapshot object provided by the Manager + connector -- connector object provided by the Manager + igroup -- name of igroup to use for exporting + + Returns: + lun_id -- the LUN ID assigned by the backend + """ + lun_id = -1 + export_to = '' + v = self.common.vip + + if igroup: + export_to = igroup + elif connector: + export_to = self._convert_wwns_openstack_to_vmem( + connector['wwpns']) + else: + raise exception.Error(_("No initiators found, cannot proceed")) + + LOG.debug("Exporting snapshot %s.", snapshot['id']) + + try: + self.common._send_cmd(v.snapshot.export_lun_snapshot, '', + self.common.container, snapshot['volume_id'], + snapshot['id'], export_to, 'all', 'auto') + + except Exception: + LOG.exception(_LE("Snapshot export for %s failed!"), + snapshot['id']) + raise + + else: + self.common._wait_for_export_config(snapshot['volume_id'], + snapshot['id'], state=True) + lun_id = self.common._get_snapshot_id(snapshot['volume_id'], + snapshot['id']) + + return lun_id + + @utils.synchronized('vmem-export') + def _unexport_snapshot(self, snapshot): + """Removes the export configuration for the given snapshot. + + The equivalent CLI command is "no snapshot export container + PROD08 lun name " + + Arguments: + snapshot -- snapshot object provided by the Manager + """ + v = self.common.vip + + LOG.debug("Unexporting snapshot %s.", snapshot['id']) + + try: + self.common._send_cmd(v.snapshot.unexport_lun_snapshot, '', + self.common.container, snapshot['volume_id'], + snapshot['id'], 'all', 'all', 'auto', False) + + except Exception: + LOG.exception(_LE("Snapshot unexport for %s failed!"), + snapshot['id']) + raise + + else: + self.common._wait_for_export_config(snapshot['volume_id'], + snapshot['id'], state=False) + + def _add_igroup_member(self, connector, igroup): + """Add an initiator to the openstack igroup so it can see exports. + + The equivalent CLI command is "igroup addto name + initiators " + + Arguments: + connector -- connector object provided by the Manager + """ + v = self.common.vip + wwpns = self._convert_wwns_openstack_to_vmem(connector['wwpns']) + + LOG.debug("Adding initiators %(wwpns)s to igroup %(igroup)s." % + {'wwpns': wwpns, 'igroup': igroup}) + + resp = v.igroup.add_initiators(igroup, wwpns) + + if resp['code'] != 0: + raise exception.Error( + _('Failed to add igroup member: %(code)d, %(message)s') % resp) + + 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 array to see if any initiator wwns still have active sessions. + + We only need to check to see if any one initiator wwn is + connected, since all initiators are connected to all targets + on a lun export for fibrechannel. + """ + v = self.common.vip + initiator_wwns = self._convert_wwns_openstack_to_vmem( + connector['wwpns']) + + bn = "/vshare/config/export/container/%s/lun/**" \ + % self.common.container + global_export_config = v.basic.get_node_values(bn) + + for node in global_export_config: + if node.endswith(initiator_wwns[0]): + return True + return False + + def _update_stats(self): + """Gathers array stats from the backend and converts them to GB values. + """ + data = {} + total_gb = 0 + free_gb = 0 + v = self.common.vip + + master_cluster_id = v.basic.get_node_values( + '/cluster/state/master_id').values()[0] + + bn1 = "/vshare/state/global/%s/container/%s/total_bytes" \ + % (master_cluster_id, self.common.container) + bn2 = "/vshare/state/global/%s/container/%s/free_bytes" \ + % (master_cluster_id, self.common.container) + resp = v.basic.get_node_values([bn1, bn2]) + + if bn1 in resp: + total_gb = resp[bn1] / units.Gi + else: + LOG.warn(_LW("Failed to receive update for total_gb stat!")) + + if bn2 in resp: + free_gb = resp[bn2] / units.Gi + else: + LOG.warn(_LW("Failed to receive update for free_gb stat!")) + + 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. + + Arguments: + mg_conn -- active XG connection to one of the gateways + + Returns: + active_gw_fcp_wwns -- list of WWNs + """ + v = self.common.vip + active_gw_fcp_wwns = [] + + gateway_ids = v.basic.get_node_values( + '/vshare/state/global/*').values() + + for i in gateway_ids: + bn = "/vshare/state/global/%d/target/fc/**" % i + resp = v.basic.get_node_values(bn) + + for node in resp: + if node.endswith('/wwn'): + active_gw_fcp_wwns.append(resp[node]) + + return self._convert_wwns_vmem_to_openstack(active_gw_fcp_wwns) + + def _convert_wwns_openstack_to_vmem(self, wwns): + """Convert a list of Openstack WWNs to VMEM compatible WWN strings. + + Input format is '50014380186b3f65', output format is + 'wwn.50:01:43:80:18:6b:3f:65'. + + Arguments: + wwns -- list of Openstack-based WWN strings. + + Returns: + output -- list of VMEM-based WWN strings. + """ + output = [] + for w in wwns: + output.append('wwn.{0}'.format( + ':'.join(w[x:x + 2] for x in xrange(0, len(w), 2)))) + return output + + def _convert_wwns_vmem_to_openstack(self, wwns): + """Convert a list of VMEM WWNs to Openstack compatible WWN strings. + + Input format is 'wwn.50:01:43:80:18:6b:3f:65', output format + is '50014380186b3f65'. + + Arguments: + wwns -- list of VMEM-based WWN strings. + + Returns: + output -- list of Openstack-based WWN strings. + """ + output = [] + for w in wwns: + output.append(''.join(w[4:].split(':'))) + return output diff --git a/cinder/volume/drivers/violin/v6000_iscsi.py b/cinder/volume/drivers/violin/v6000_iscsi.py new file mode 100644 index 00000000000..fe594cbab26 --- /dev/null +++ b/cinder/volume/drivers/violin/v6000_iscsi.py @@ -0,0 +1,598 @@ +# Copyright 2013 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 iSCSI Driver for Openstack Cinder + +Provides iSCSI specific LUN services for V6000 series flash arrays. + +This driver requires VMOS v6.3.0.4 or newer software on the array. + +You will need to install the python xg-tools client: +sudo pip install xg-tools + +Set the following in the cinder.conf file to enable the VMEM V6000 +ISCSI Driver along with the required flags: + +volume_driver=cinder.volume.drivers.violin.v6000_iscsi.V6000ISCSIDriver + +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 random + +from oslo.utils import units + +from cinder import context +from cinder.db.sqlalchemy import models +from cinder import exception +from cinder.i18n import _, _LE, _LI, _LW +from cinder.openstack.common import log as logging +from cinder.openstack.common import loopingcall +from cinder import utils +from cinder.volume import driver +from cinder.volume.drivers.san import san +from cinder.volume.drivers.violin import v6000_common + +LOG = logging.getLogger(__name__) + + +class V6000ISCSIDriver(driver.ISCSIDriver): + """Executes commands relating to iSCSI-based Violin Memory Arrays. + + Version history: + 1.0 - Initial driver + """ + + VERSION = '1.0' + + def __init__(self, *args, **kwargs): + super(V6000ISCSIDriver, self).__init__(*args, **kwargs) + self.array_info = [] + self.gateway_iscsi_ip_addresses_mga = [] + self.gateway_iscsi_ip_addresses_mgb = [] + self.stats = {} + self.configuration.append_config_values(v6000_common.violin_opts) + self.configuration.append_config_values(san.san_opts) + self.common = v6000_common.V6000Common(self.configuration) + + 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(V6000ISCSIDriver, self).do_setup(context) + self.common.do_setup(context) + + self.gateway_iscsi_ip_addresses_mga = self._get_active_iscsi_ips( + self.common.mga) + for ip in self.gateway_iscsi_ip_addresses_mga: + self.array_info.append({"node": self._get_hostname('mga'), + "addr": ip, + "conn": self.common.mga}) + self.gateway_iscsi_ip_addresses_mgb = self._get_active_iscsi_ips( + self.common.mgb) + for ip in self.gateway_iscsi_ip_addresses_mgb: + self.array_info.append({"node": self._get_hostname('mgb'), + "addr": ip, + "conn": self.common.mgb}) + + def check_for_setup_error(self): + """Returns an error if prerequisites aren't met.""" + self.common.check_for_setup_error() + + bn = "/vshare/config/iscsi/enable" + resp = self.common.vip.basic.get_node_values(bn) + if resp[bn] is not True: + raise exception.ViolinInvalidBackendConfig( + reason=_('iSCSI is not enabled')) + if len(self.gateway_iscsi_ip_addresses_mga) == 0: + raise exception.ViolinInvalidBackendConfig( + reason=_('no available iSCSI IPs on mga')) + if len(self.gateway_iscsi_ip_addresses_mgb) == 0: + raise exception.ViolinInvalidBackendConfig( + reason=_('no available iSCSI IPs on mgb')) + + def create_volume(self, volume): + """Creates a volume.""" + self.common._create_lun(volume) + + def delete_volume(self, volume): + """Deletes a volume.""" + self.common._delete_lun(volume) + + def extend_volume(self, volume, new_size): + """Deletes a volume.""" + self.common._extend_lun(volume, new_size) + + def create_snapshot(self, snapshot): + """Creates a snapshot from an existing volume.""" + self.common._create_lun_snapshot(snapshot) + + def delete_snapshot(self, snapshot): + """Deletes a snapshot.""" + self.common._delete_lun_snapshot(snapshot) + + def create_volume_from_snapshot(self, volume, snapshot): + """Creates a volume from a snapshot.""" + ctxt = context.get_admin_context() + snapshot['size'] = snapshot['volume']['size'] + self.common._create_lun(volume) + self.copy_volume_data(ctxt, snapshot, volume) + + def create_cloned_volume(self, volume, src_vref): + """Creates a full clone of the specified volume.""" + ctxt = context.get_admin_context() + self.common._create_lun(volume) + self.copy_volume_data(ctxt, src_vref, volume) + + 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 + + def initialize_connection(self, volume, connector): + """Initializes the connection (target<-->initiator).""" + igroup = None + + if self.configuration.use_igroups: + # + # Most drivers don't use igroups, because there are a + # number of issues with multipathing and iscsi/fcp where + # lun devices either aren't cleaned up properly or are + # stale (from previous scans). + # + + # If the customer really wants igroups for whatever + # reason, we create a new igroup for each host/hypervisor. + # Every lun that is exported to the particular + # hypervisor/host will be contained in this igroup. This + # should prevent other hosts from seeing luns they aren't + # using when they perform scans. + # + igroup = self.common._get_igroup(volume, connector) + self._add_igroup_member(connector, igroup) + + vol = self._get_short_name(volume['id']) + tgt = self._create_iscsi_target(volume) + if isinstance(volume, models.Volume): + lun = self._export_lun(volume, connector, igroup) + else: + lun = self._export_snapshot(volume, connector, igroup) + + iqn = "%s%s:%s" % (self.configuration.iscsi_target_prefix, + tgt['node'], vol) + self.common.vip.basic.save_config() + + properties = {} + properties['target_discovered'] = False + properties['target_portal'] = '%s:%d' \ + % (tgt['addr'], self.configuration.iscsi_port) + properties['target_iqn'] = iqn + properties['target_lun'] = lun + properties['volume_id'] = volume['id'] + properties['auth_method'] = 'CHAP' + properties['auth_username'] = '' + properties['auth_password'] = '' + + return {'driver_volume_type': 'iscsi', 'data': properties} + + def terminate_connection(self, volume, connector, force=False, **kwargs): + """Terminates the connection (target<-->initiator).""" + if isinstance(volume, models.Volume): + self._unexport_lun(volume) + else: + self._unexport_snapshot(volume) + self._delete_iscsi_target(volume) + self.common.vip.basic.save_config() + + def get_volume_stats(self, refresh=False): + """Get volume stats.""" + if refresh or not self.stats: + self._update_stats() + return self.stats + + @utils.synchronized('vmem-export') + def _create_iscsi_target(self, volume): + """Creates a new target for use in exporting a lun. + + Openstack does not yet support multipathing. We still create + HA targets but we pick a single random target for the + Openstack infrastructure to use. This at least allows us to + evenly distribute LUN connections across the storage cluster. + The equivalent CLI commands are "iscsi target create + " and "iscsi target bind to + ". + + Arguments: + volume -- volume object provided by the Manager + + Returns: + reference to randomly selected target object + """ + v = self.common.vip + target_name = self._get_short_name(volume['id']) + + LOG.debug("Creating iscsi target %s.", target_name) + + try: + self.common._send_cmd_and_verify(v.iscsi.create_iscsi_target, + self._wait_for_targetstate, + '', [target_name], [target_name]) + + except Exception: + LOG.exception(_LE("Failed to create iscsi target!")) + raise + + try: + self.common._send_cmd(self.common.mga.iscsi.bind_ip_to_target, + '', target_name, + self.gateway_iscsi_ip_addresses_mga) + self.common._send_cmd(self.common.mgb.iscsi.bind_ip_to_target, + '', target_name, + self.gateway_iscsi_ip_addresses_mgb) + except Exception: + LOG.exception(_LE("Failed to bind iSCSI targets!")) + raise + + return self.array_info[random.randint(0, len(self.array_info) - 1)] + + @utils.synchronized('vmem-export') + def _delete_iscsi_target(self, volume): + """Deletes the iscsi target for a lun. + + The CLI equivalent is "no iscsi target create ". + + Arguments: + volume -- volume object provided by the Manager + """ + v = self.common.vip + success_msgs = ['', 'Invalid target'] + target_name = self._get_short_name(volume['id']) + + LOG.debug("Deleting iscsi target for %s.", target_name) + + try: + self.common._send_cmd(v.iscsi.delete_iscsi_target, + success_msgs, target_name) + except Exception: + LOG.exception(_LE("Failed to delete iSCSI target!")) + raise + + @utils.synchronized('vmem-export') + def _export_lun(self, volume, connector=None, igroup=None): + """Generates the export configuration for the given volume. + + The equivalent CLI command is "lun export container + name " + + Arguments: + volume -- volume object provided by the Manager + connector -- connector object provided by the Manager + igroup -- name of igroup to use for exporting + + Returns: + lun_id -- the LUN ID assigned by the backend + """ + lun_id = -1 + export_to = '' + v = self.common.vip + + if igroup: + export_to = igroup + elif connector: + export_to = connector['initiator'] + else: + raise exception.Error(_("No initiators found, cannot proceed")) + + target_name = self._get_short_name(volume['id']) + + LOG.debug("Exporting lun %s." % volume['id']) + + try: + self.common._send_cmd_and_verify( + v.lun.export_lun, self.common._wait_for_export_config, '', + [self.common.container, volume['id'], target_name, + export_to, 'auto'], [volume['id'], 'state=True']) + + except Exception: + LOG.exception(_LE("LUN export for %s failed!"), volume['id']) + raise + + lun_id = self.common._get_lun_id(volume['id']) + + return lun_id + + @utils.synchronized('vmem-export') + def _unexport_lun(self, volume): + """Removes the export configuration for the given volume. + + The equivalent CLI command is "no lun export container + name " + + Arguments: + volume -- volume object provided by the Manager + """ + v = self.common.vip + + LOG.debug("Unexporting lun %s.", volume['id']) + + try: + self.common._send_cmd_and_verify( + v.lun.unexport_lun, self.common._wait_for_export_config, '', + [self.common.container, volume['id'], 'all', 'all', 'auto'], + [volume['id'], 'state=False']) + + except exception.ViolinBackendErrNotFound: + LOG.debug("Lun %s already unexported, continuing.", volume['id']) + + except Exception: + LOG.exception(_LE("LUN unexport for %s failed!"), volume['id']) + raise + + @utils.synchronized('vmem-export') + def _export_snapshot(self, snapshot, connector=None, igroup=None): + """Generates the export configuration for the given snapshot. + + The equivalent CLI command is "snapshot export container + PROD08 lun name " + + Arguments: + snapshot -- snapshot object provided by the Manager + connector -- connector object provided by the Manager + igroup -- name of igroup to use for exporting + + Returns: + lun_id -- the LUN ID assigned by the backend + """ + lun_id = -1 + export_to = '' + v = self.common.vip + + target_name = self._get_short_name(snapshot['id']) + + LOG.debug("Exporting snapshot %s.", snapshot['id']) + + if igroup: + export_to = igroup + elif connector: + export_to = connector['initiator'] + else: + raise exception.Error(_("No initiators found, cannot proceed")) + + try: + self.common._send_cmd(v.snapshot.export_lun_snapshot, '', + self.common.container, snapshot['volume_id'], + snapshot['id'], export_to, target_name, + 'auto') + + except Exception: + LOG.exception(_LE("Snapshot export for %s failed!"), + snapshot['id']) + raise + + else: + self.common._wait_for_export_config(snapshot['volume_id'], + snapshot['id'], state=True) + lun_id = self.common._get_snapshot_id(snapshot['volume_id'], + snapshot['id']) + + return lun_id + + @utils.synchronized('vmem-export') + def _unexport_snapshot(self, snapshot): + """Removes the export configuration for the given snapshot. + + The equivalent CLI command is "no snapshot export container + PROD08 lun name " + + Arguments: + snapshot -- snapshot object provided by the Manager + """ + v = self.common.vip + + LOG.debug("Unexporting snapshot %s.", snapshot['id']) + + try: + self.common._send_cmd(v.snapshot.unexport_lun_snapshot, '', + self.common.container, snapshot['volume_id'], + snapshot['id'], 'all', 'all', 'auto', False) + + except Exception: + LOG.exception(_LE("Snapshot unexport for %s failed!"), + snapshot['id']) + raise + + else: + self.common._wait_for_export_config(snapshot['volume_id'], + snapshot['id'], state=False) + + def _add_igroup_member(self, connector, igroup): + """Add an initiator to an igroup so it can see exports. + + The equivalent CLI command is "igroup addto name + initiators " + + Arguments: + connector -- connector object provided by the Manager + """ + v = self.common.vip + + LOG.debug("Adding initiator %s to igroup.", connector['initiator']) + + resp = v.igroup.add_initiators(igroup, connector['initiator']) + + if resp['code'] != 0: + raise exception.Error( + _('Failed to add igroup member: %(code)d, %(message)s') % resp) + + def _update_stats(self): + """Gathers array stats from the backend and converts them to GB values. + """ + data = {} + total_gb = 0 + free_gb = 0 + v = self.common.vip + + master_cluster_id = v.basic.get_node_values( + '/cluster/state/master_id').values()[0] + + bn1 = "/vshare/state/global/%s/container/%s/total_bytes" \ + % (master_cluster_id, self.common.container) + bn2 = "/vshare/state/global/%s/container/%s/free_bytes" \ + % (master_cluster_id, self.common.container) + resp = v.basic.get_node_values([bn1, bn2]) + + if bn1 in resp: + total_gb = resp[bn1] / units.Gi + else: + LOG.warn(_LW("Failed to receive update for total_gb stat!")) + + if bn2 in resp: + free_gb = resp[bn2] / units.Gi + else: + LOG.warn(_LW("Failed to receive update for free_gb stat!")) + + 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'] = 'iSCSI' + 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_short_name(self, volume_name): + """Creates a vSHARE-compatible iSCSI target name. + + The Folsom-style volume names are prefix(7) + uuid(36), which + is too long for vSHARE for target names. To keep things + simple we can just truncate the name to 32 chars. + + Arguments: + volume_name -- name of volume/lun + + Returns: + Shortened volume name as a string. + """ + return volume_name[:32] + + def _get_active_iscsi_ips(self, mg_conn): + """Get a list of gateway IP addresses that can be used for iSCSI. + + Arguments: + mg_conn -- active XG connection to one of the gateways + + Returns: + active_gw_iscsi_ips -- list of IP addresses + """ + active_gw_iscsi_ips = [] + interfaces_to_skip = ['lo', 'vlan10', 'eth1', 'eth2', 'eth3'] + + bn = "/net/interface/config/*" + intf_list = mg_conn.basic.get_node_values(bn) + + for i in intf_list: + if intf_list[i] in interfaces_to_skip: + continue + + bn1 = "/net/interface/state/%s/addr/ipv4/1/ip" % intf_list[i] + bn2 = "/net/interface/state/%s/flags/link_up" % intf_list[i] + resp = mg_conn.basic.get_node_values([bn1, bn2]) + + if len(resp.keys()) == 2 and resp[bn2] is True: + active_gw_iscsi_ips.append(resp[bn1]) + + return active_gw_iscsi_ips + + def _get_hostname(self, mg_to_query=None): + """Get the hostname of one of the mgs (hostname is used in IQN). + + If the remote query fails then fall back to using the hostname + provided in the cinder configuration file. + + Arguments: + mg_to_query -- name of gateway to query 'mga' or 'mgb' + + Returns: hostname -- hostname as a string + """ + hostname = self.configuration.san_ip + conn = self.common.vip + + if mg_to_query == "mga": + hostname = self.configuration.gateway_mga + conn = self.common.mga + elif mg_to_query == "mgb": + hostname = self.configuration.gateway_mgb + conn = self.common.mgb + + ret_dict = conn.basic.get_node_values("/system/hostname") + if ret_dict: + hostname = ret_dict.items()[0][1] + else: + LOG.debug("Unable to fetch gateway hostname for %s." % mg_to_query) + + return hostname + + def _wait_for_targetstate(self, target_name): + """Polls backend to verify an iscsi target configuration. + + This function will try to verify the creation of an iscsi + target on both gateway nodes of the array every 5 seconds. + + Arguments: + target_name -- name of iscsi target to be polled + + Returns: + True if the export state was correctly added + """ + bn = "/vshare/config/iscsi/target/%s" % (target_name) + + def _loop_func(): + status = [False, False] + mg_conns = [self.common.mga, self.common.mgb] + + LOG.debug("Entering _wait_for_targetstate loop: target=%s.", + target_name) + + for node_id in xrange(2): + resp = mg_conns[node_id].basic.get_node_values(bn) + if len(resp.keys()): + status[node_id] = True + + if status[0] and status[1]: + raise loopingcall.LoopingCallDone(retvalue=True) + + timer = loopingcall.FixedIntervalLoopingCall(_loop_func) + success = timer.start(interval=5).wait() + + return success