]> review.fuel-infra Code Review - openstack-build/cinder-build.git/commitdiff
Fibrechannel and iSCSI for Violin Memory 6000 Series Arrays
authorRyan Lucio <rlucio@vmem.com>
Sat, 6 Dec 2014 11:33:27 +0000 (03:33 -0800)
committerRyan Lucio <rlucio@vmem.com>
Wed, 17 Dec 2014 12:55:06 +0000 (04:55 -0800)
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
cinder/tests/fake_vmem_xgtools_client.py [new file with mode: 0644]
cinder/tests/test_v6000_common.py [new file with mode: 0644]
cinder/tests/test_v6000_fcp.py [new file with mode: 0644]
cinder/tests/test_v6000_iscsi.py [new file with mode: 0644]
cinder/volume/drivers/violin/__init__.py [new file with mode: 0644]
cinder/volume/drivers/violin/v6000_common.py [new file with mode: 0644]
cinder/volume/drivers/violin/v6000_fcp.py [new file with mode: 0644]
cinder/volume/drivers/violin/v6000_iscsi.py [new file with mode: 0644]

index 7c746cf93881af9c4131417c7727dbdd8ad8be23..3373752d7d43c85ffe994aa6aed4d808cae17152 100755 (executable)
@@ -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 (file)
index 0000000..3531650
--- /dev/null
@@ -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 (file)
index 0000000..6982239
--- /dev/null
@@ -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 (file)
index 0000000..34d5092
--- /dev/null
@@ -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 (file)
index 0000000..2c9f8e7
--- /dev/null
@@ -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 (file)
index 0000000..e69de29
diff --git a/cinder/volume/drivers/violin/v6000_common.py b/cinder/volume/drivers/violin/v6000_common.py
new file mode 100644 (file)
index 0000000..6872659
--- /dev/null
@@ -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
+        <container_name> name <lun_name> size <gb>"
+
+        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
+        <container_name> name <lun_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
+        <container_name> name <lun_name> size <gb>"
+
+        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
+        <container> lun <volume_name> name <snapshot_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
+        <container> lun <volume_name> name <snapshot_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 (file)
index 0000000..aeedf49
--- /dev/null
@@ -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
+        <container_name> name <lun_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
+        <container_name> name <lun_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 <snapshot_name> name <volume_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 <snapshot_name> name <volume_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 <igroup_name>
+        initiators <initiator_name>"
+
+        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 (file)
index 0000000..fe594cb
--- /dev/null
@@ -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
+        <target_name>" and "iscsi target bind <target_name> to
+        <ip_of_mg_eth_intf>".
+
+        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 <target_name>".
+
+        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
+        <container_name> name <lun_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
+        <container_name> name <lun_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 <snapshot_name> name <volume_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 <snapshot_name> name <volume_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 <igroup_name>
+        initiators <initiator_name>"
+
+        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