Fibrechannel and iSCSI for Violin Memory 6000 Series Arrays

This patch adds both Fibrechannel and iSCSI driver support for Violin
Memory 6000 Series All-Flash Arrays.

Certification results posted at:
https://bugs.launchpad.net/cinder/+bug/1399911

Change-Id: I9f62fe79e892cff01abdb02dbd95be8e432f8ab7
Implements: blueprint violinmemory-v6000-storage-drivers
This commit is contained in:
Ryan Lucio 2014-12-06 03:33:27 -08:00
parent d1b89778f9
commit 4a679cc7f1
9 changed files with 3668 additions and 0 deletions

View File

@ -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")

View File

@ -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',
]

View File

@ -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))

View File

@ -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)

View File

@ -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)

View File

View File

@ -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'])

View File

@ -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

View File

@ -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