Add cinder volume driver support for Nimble Storage.

The set of changes introduces the cinder iSCSI volume driver support for
Nimble Storage. It implements the minimum set of features required by the
icehouse release. Please see the blueprint nimble-iscsi-volume-driver for
more information.

Change-Id: I48781fc890b8fefb33027da3db2b1ff56c5deed4
Implements: blueprint nimble-iscsi-volume-driver
This commit is contained in:
Jay Wang 2014-02-13 07:37:42 -08:00
parent 350334dec0
commit 62f5e2803a
3 changed files with 1271 additions and 0 deletions

554
cinder/tests/test_nimble.py Normal file
View File

@ -0,0 +1,554 @@
# Nimble Storage, Inc. (c) 2013-2014
# 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.
import mock
from oslo.config import cfg
from cinder import exception
from cinder.openstack.common import log as logging
from cinder import test
from cinder.volume.drivers import nimble
CONF = cfg.CONF
NIMBLE_CLIENT = 'cinder.volume.drivers.nimble.client'
NIMBLE_URLLIB2 = 'cinder.volume.drivers.nimble.urllib2'
NIMBLE_RANDOM = 'cinder.volume.drivers.nimble.random'
LOG = logging.getLogger(__name__)
FAKE_ENUM_STRING = """
<simpleType name="SmErrorType">
<restriction base="xsd:string">
<enumeration value="SM-ok"/><!-- enum const = 0 -->
<enumeration value="SM-eperm"/><!-- enum const = 1 -->
<enumeration value="SM-enoent"/><!-- enum const = 2 -->
<enumeration value="SM-eaccess"/><!-- enum const = 13 -->
<enumeration value="SM-eexist"/><!-- enum const = 17 -->
</restriction>
</simpleType>"""
FAKE_POSITIVE_LOGIN_RESPONSE_1 = {'err-list': {'err-list':
[{'code': 0}]},
'authInfo': {'sid': "a9b9aba7"}}
FAKE_POSITIVE_LOGIN_RESPONSE_2 = {'err-list': {'err-list':
[{'code': 0}]},
'authInfo': {'sid': "a9f3eba7"}}
FAKE_POSITIVE_NETCONFIG_RESPONSE = {
'config': {'subnet-list': [{'label': "data1",
'subnet-id': {'type': 3},
'discovery-ip': "172.18.108.21"},
{'label': "mgmt-data",
'subnet-id':
{'type': 4},
'discovery-ip': "10.18.108.55"}]},
'err-list': {'err-list': [{'code': 0}]}}
FAKE_NEGATIVE_NETCONFIG_RESPONSE = {'err-list': {'err-list':
[{'code': 13}]}}
FAKE_CREATE_VOLUME_POSITIVE_RESPONSE = {'err-list': {'err-list':
[{'code': 0}]},
'name': "openstack-test11"}
FAKE_CREATE_VOLUME_NEGATIVE_RESPONSE = {'err-list': {'err-list':
[{'code': 17}]},
'name': "openstack-test11"}
FAKE_GENERIC_POSITIVE_RESPONSE = {'err-list': {'err-list':
[{'code': 0}]}}
FAKE_POSITIVE_GROUP_CONFIG_RESPONSE = {
'err-list': {'err-list': [{'code': 0}]},
'info': {'usableCapacity': 8016883089408,
'volUsageCompressed': 2938311843,
'snapUsageCompressed': 36189,
'unusedReserve': 0,
'spaceInfoValid': True}}
FAKE_IGROUP_LIST_RESPONSE = {
'err-list': {'err-list': [{'code': 0}]},
'initiatorgrp-list': [
{'initiator-list': [{'name': 'test-initiator1'},
{'name': 'test-initiator2'}],
'name': 'test-igrp1'},
{'initiator-list': [{'name': 'test-initiator1'}],
'name': 'test-igrp2'}]}
FAKE_GET_VOL_INFO_RESPONSE = {'err-list': {'err-list':
[{'code': 0}]},
'vol': {'target-name': 'iqn.test'}}
def create_configuration(username, password, ip_address,
pool_name=None, subnet_label=None,
thin_provision=True):
configuration = mock.Mock()
configuration.san_login = username
configuration.san_password = password
configuration.san_ip = ip_address
configuration.san_thin_provision = thin_provision
configuration.nimble_pool_name = pool_name
configuration.nimble_subnet_label = subnet_label
configuration.safe_get.return_value = 'NIMBLE'
return configuration
class NimbleDriverBaseTestCase(test.TestCase):
"""Base Class for the NimbleDriver Tests."""
def setUp(self):
super(NimbleDriverBaseTestCase, self).setUp()
self.mock_client_service = None
self.mock_client_class = None
self.driver = None
@staticmethod
def client_mock_decorator(configuration):
def client_mock_wrapper(func):
def inner_client_mock(
self, mock_client_class, mock_urllib2, *args, **kwargs):
self.mock_client_class = mock_client_class
self.mock_client_service = mock.MagicMock(name='Client')
self.mock_client_class.Client.return_value = \
self.mock_client_service
mock_wsdl = mock_urllib2.urlopen.return_value
mock_wsdl.read = mock.MagicMock()
mock_wsdl.read.return_value = FAKE_ENUM_STRING
self.driver = nimble.NimbleISCSIDriver(
configuration=configuration)
self.mock_client_service.service.login.return_value = \
FAKE_POSITIVE_LOGIN_RESPONSE_1
self.driver.do_setup(None)
func(self, *args, **kwargs)
return inner_client_mock
return client_mock_wrapper
def tearDown(self):
super(NimbleDriverBaseTestCase, self).tearDown()
class NimbleDriverLoginTestCase(NimbleDriverBaseTestCase):
"""Tests do_setup api."""
@mock.patch(NIMBLE_URLLIB2)
@mock.patch(NIMBLE_CLIENT)
@NimbleDriverBaseTestCase.client_mock_decorator(create_configuration(
'nimble', 'nimble_pass', '10.18.108.55', 'default', '*'))
def test_do_setup_positive(self):
expected_call_list = [
mock.call.Client(
'https://10.18.108.55/wsdl/NsGroupManagement.wsdl',
username='nimble',
password='nimble_pass')]
self.assertEqual(self.mock_client_class.method_calls,
expected_call_list)
expected_call_list = [mock.call.set_options(
location='https://10.18.108.55:5391/soap'),
mock.call.service.login(
req={'username': 'nimble', 'password': 'nimble_pass'})]
self.assertEqual(
self.mock_client_service.method_calls,
expected_call_list)
@mock.patch(NIMBLE_URLLIB2)
@mock.patch(NIMBLE_CLIENT)
@NimbleDriverBaseTestCase.client_mock_decorator(create_configuration(
'nimble', 'nimble_pass', '10.18.108.55', 'default', '*'))
def test_expire_session_id(self):
self.mock_client_service.service.login.return_value = \
FAKE_POSITIVE_LOGIN_RESPONSE_2
self.mock_client_service.service.getNetConfig = mock.MagicMock(
side_effect=[
FAKE_NEGATIVE_NETCONFIG_RESPONSE,
FAKE_POSITIVE_NETCONFIG_RESPONSE])
self.driver.APIExecutor.get_netconfig("active")
expected_call_list = [mock.call.set_options(
location='https://10.18.108.55:5391/soap'),
mock.call.service.login(
req={
'username': 'nimble', 'password': 'nimble_pass'}),
mock.call.service.getNetConfig(
request={'name': 'active',
'sid': 'a9b9aba7'}),
mock.call.service.login(
req={'username': 'nimble',
'password': 'nimble_pass'}),
mock.call.service.getNetConfig(
request={'name': 'active', 'sid': 'a9f3eba7'})]
self.assertEqual(
self.mock_client_service.method_calls,
expected_call_list)
class NimbleDriverVolumeTestCase(NimbleDriverBaseTestCase):
"""Tests volume related api's."""
@mock.patch(NIMBLE_URLLIB2)
@mock.patch(NIMBLE_CLIENT)
@NimbleDriverBaseTestCase.client_mock_decorator(create_configuration(
'nimble', 'nimble_pass', '10.18.108.55', 'default', '*'))
def test_create_volume_positive(self):
self.mock_client_service.service.createVol.return_value = \
FAKE_CREATE_VOLUME_POSITIVE_RESPONSE
self.mock_client_service.service.getVolInfo.return_value = \
FAKE_GET_VOL_INFO_RESPONSE
self.mock_client_service.service.getNetConfig.return_value = \
FAKE_POSITIVE_NETCONFIG_RESPONSE
self.assertEqual({
'provider_location': '172.18.108.21:3260 iqn.test 0',
'provider_auth': None},
self.driver.create_volume({'name': 'testvolume',
'size': 1}))
self.mock_client_service.service.createVol.assert_called_once_with(
request={
'attr': {'snap-quota': 1073741824, 'warn-level': 858993459,
'name': 'testvolume', 'reserve': 0,
'online': True, 'pool-name': 'default',
'size': 1073741824, 'quota': 1073741824,
'perfpol-name': 'default', 'description': ''},
'sid': 'a9b9aba7'})
@mock.patch(NIMBLE_URLLIB2)
@mock.patch(NIMBLE_CLIENT)
@NimbleDriverBaseTestCase.client_mock_decorator(create_configuration(
'nimble', 'nimble_pass', '10.18.108.55', 'default', '*'))
def test_create_volume_negative(self):
self.mock_client_service.service.createVol.return_value = \
FAKE_CREATE_VOLUME_NEGATIVE_RESPONSE
self.assertRaises(
exception.VolumeBackendAPIException,
self.driver.create_volume,
{'name': 'testvolume',
'size': 1})
@mock.patch(NIMBLE_URLLIB2)
@mock.patch(NIMBLE_CLIENT)
@NimbleDriverBaseTestCase.client_mock_decorator(create_configuration(
'nimble', 'nimble_pass', '10.18.108.55', 'default', '*'))
def test_delete_volume(self):
self.mock_client_service.service.onlineVol.return_value = \
FAKE_GENERIC_POSITIVE_RESPONSE
self.mock_client_service.service.deleteVol.return_value = \
FAKE_GENERIC_POSITIVE_RESPONSE
self.mock_client_service.service.dissocProtPol.return_value = \
FAKE_GENERIC_POSITIVE_RESPONSE
self.driver.delete_volume({'name': 'testvolume'})
expected_calls = [mock.call.service.onlineVol(
request={
'online': False, 'name': 'testvolume', 'sid': 'a9b9aba7'}),
mock.call.service.dissocProtPol(
request={'vol-name': 'testvolume', 'sid': 'a9b9aba7'}),
mock.call.service.deleteVol(
request={'name': 'testvolume', 'sid': 'a9b9aba7'})]
self.mock_client_service.assert_has_calls(expected_calls)
@mock.patch(NIMBLE_URLLIB2)
@mock.patch(NIMBLE_CLIENT)
@NimbleDriverBaseTestCase.client_mock_decorator(create_configuration(
'nimble', 'nimble_pass', '10.18.108.55', 'default', '*'))
def test_extend_volume(self):
self.mock_client_service.service.editVol.return_value = \
FAKE_CREATE_VOLUME_POSITIVE_RESPONSE
self.driver.extend_volume({'name': 'testvolume'}, 5)
self.mock_client_service.service.editVol.assert_called_once_with(
request={'attr': {'size': 5368709120,
'snap-quota': 5368709120,
'warn-level': 4294967296,
'reserve': 0,
'quota': 5368709120},
'mask': 628,
'name': 'testvolume',
'sid': 'a9b9aba7'})
@mock.patch(NIMBLE_RANDOM)
@mock.patch(NIMBLE_URLLIB2)
@mock.patch(NIMBLE_CLIENT)
@NimbleDriverBaseTestCase.client_mock_decorator(create_configuration(
'nimble', 'nimble_pass', '10.18.108.55', 'default', '*', False))
def test_create_cloned_volume(self, mock_random):
mock_random.sample.return_value = 'abcdefghijkl'
self.mock_client_service.service.snapVol.return_value = \
FAKE_GENERIC_POSITIVE_RESPONSE
self.mock_client_service.service.cloneVol.return_value = \
FAKE_GENERIC_POSITIVE_RESPONSE
self.mock_client_service.service.getVolInfo.return_value = \
FAKE_GET_VOL_INFO_RESPONSE
self.mock_client_service.service.getNetConfig.return_value = \
FAKE_POSITIVE_NETCONFIG_RESPONSE
self.assertEqual({
'provider_location': '172.18.108.21:3260 iqn.test 0',
'provider_auth': None},
self.driver.create_cloned_volume({'name': 'volume',
'size': 5},
{'name': 'testvolume',
'size': 5}))
expected_calls = [mock.call.service.snapVol(
request={
'vol': 'testvolume',
'snapAttr': {'name': 'openstack-clone-volume-abcdefghijkl',
'description': ''},
'sid': 'a9b9aba7'}),
mock.call.service.cloneVol(
request={
'snap-name': 'openstack-clone-volume-abcdefghijkl',
'attr': {'snap-quota': 5368709120,
'name': 'volume',
'quota': 5368709120,
'reserve': 5368709120,
'online': True,
'warn-level': 4294967296,
'perfpol-name': 'default'},
'name': 'testvolume',
'sid': 'a9b9aba7'})]
self.mock_client_service.assert_has_calls(expected_calls)
@mock.patch(NIMBLE_URLLIB2)
@mock.patch(NIMBLE_CLIENT)
@NimbleDriverBaseTestCase.client_mock_decorator(create_configuration(
'nimble', 'nimble_pass', '10.18.108.55', 'default', '*'))
def test_get_volume_stats(self):
self.mock_client_service.service.getGroupConfig.return_value = \
FAKE_POSITIVE_GROUP_CONFIG_RESPONSE
expected_res = {'driver_version': '1.0',
'total_capacity_gb': 7466.30419921875,
'QoS_support': False,
'reserved_percentage': 0,
'vendor_name': 'Nimble',
'volume_backend_name': 'NIMBLE',
'storage_protocol': 'iSCSI',
'free_capacity_gb': 7463.567649364471}
self.assertEqual(
expected_res,
self.driver.get_volume_stats(refresh=True))
class NimbleDriverSnapshotTestCase(NimbleDriverBaseTestCase):
"""Tests snapshot related api's."""
@mock.patch(NIMBLE_URLLIB2)
@mock.patch(NIMBLE_CLIENT)
@NimbleDriverBaseTestCase.client_mock_decorator(create_configuration(
'nimble', 'nimble_pass', '10.18.108.55', 'default', '*'))
def test_create_snapshot(self):
self.mock_client_service.service.snapVol.return_value = \
FAKE_GENERIC_POSITIVE_RESPONSE
self.driver.create_snapshot(
{'volume_name': 'testvolume',
'name': 'testvolume-snap1'})
self.mock_client_service.service.snapVol.assert_called_once_with(
request={'vol': 'testvolume',
'snapAttr': {'name': 'testvolume-snap1',
'description':
''
},
'sid': 'a9b9aba7'})
@mock.patch(NIMBLE_URLLIB2)
@mock.patch(NIMBLE_CLIENT)
@NimbleDriverBaseTestCase.client_mock_decorator(create_configuration(
'nimble', 'nimble_pass', '10.18.108.55', 'default', '*'))
def test_delete_snapshot(self):
self.mock_client_service.service.onlineSnap.return_value = \
FAKE_GENERIC_POSITIVE_RESPONSE
self.mock_client_service.service.deleteSnap.return_value = \
FAKE_GENERIC_POSITIVE_RESPONSE
self.driver.delete_snapshot(
{'volume_name': 'testvolume',
'name': 'testvolume-snap1'})
expected_calls = [mock.call.service.onlineSnap(
request={
'vol': 'testvolume',
'online': False,
'name': 'testvolume-snap1',
'sid': 'a9b9aba7'}),
mock.call.service.deleteSnap(request={'vol': 'testvolume',
'name': 'testvolume-snap1',
'sid': 'a9b9aba7'})]
self.mock_client_service.assert_has_calls(expected_calls)
@mock.patch(NIMBLE_URLLIB2)
@mock.patch(NIMBLE_CLIENT)
@NimbleDriverBaseTestCase.client_mock_decorator(create_configuration(
'nimble', 'nimble_pass', '10.18.108.55', 'default', '*'))
def test_create_volume_from_snapshot(self):
self.mock_client_service.service.cloneVol.return_value = \
FAKE_GENERIC_POSITIVE_RESPONSE
self.mock_client_service.service.getVolInfo.return_value = \
FAKE_GET_VOL_INFO_RESPONSE
self.mock_client_service.service.getNetConfig.return_value = \
FAKE_POSITIVE_NETCONFIG_RESPONSE
self.assertEqual({
'provider_location': '172.18.108.21:3260 iqn.test 0',
'provider_auth': None},
self.driver.create_volume_from_snapshot(
{'name': 'clone-testvolume',
'size': 2},
{'volume_name': 'testvolume',
'name': 'testvolume-snap1',
'volume_size': 1}))
expected_calls = [
mock.call.service.cloneVol(
request={'snap-name': 'testvolume-snap1',
'attr': {'snap-quota': 1073741824,
'name': 'clone-testvolume',
'quota': 1073741824,
'online': True,
'reserve': 0,
'warn-level': 858993459,
'perfpol-name': 'default'},
'name': 'testvolume',
'sid': 'a9b9aba7'}),
mock.call.service.editVol(
request={'attr': {'size': 2147483648,
'snap-quota': 2147483648,
'warn-level': 1717986918,
'reserve': 0,
'quota': 2147483648},
'mask': 628,
'name': 'clone-testvolume',
'sid': 'a9b9aba7'})]
self.mock_client_service.assert_has_calls(expected_calls)
class NimbleDriverConnectionTestCase(NimbleDriverBaseTestCase):
"""Tests Connection related api's."""
@mock.patch(NIMBLE_URLLIB2)
@mock.patch(NIMBLE_CLIENT)
@NimbleDriverBaseTestCase.client_mock_decorator(create_configuration(
'nimble', 'nimble_pass', '10.18.108.55', 'default', '*'))
def test_initialize_connection_igroup_exist(self):
self.mock_client_service.service.getInitiatorGrpList.return_value = \
FAKE_IGROUP_LIST_RESPONSE
expected_res = {
'driver_volume_type': 'iscsi',
'data': {
'target_lun': '14',
'volume_id': 12,
'target_iqn': '13',
'target_discovered': False,
'target_portal': '12'}}
self.assertEqual(
expected_res,
self.driver.initialize_connection(
{'name': 'test-volume',
'provider_location': '12 13 14',
'id': 12},
{'initiator': 'test-initiator1'}))
expected_call_list = [mock.call.set_options(
location='https://10.18.108.55:5391/soap'),
mock.call.service.login(
req={
'username': 'nimble', 'password': 'nimble_pass'}),
mock.call.service.getInitiatorGrpList(
request={'sid': 'a9b9aba7'}),
mock.call.service.addVolAcl(
request={'volname': 'test-volume',
'apply-to': 3,
'chapuser': '*',
'initiatorgrp': 'test-igrp2',
'sid': 'a9b9aba7'})]
self.assertEqual(
self.mock_client_service.method_calls,
expected_call_list)
@mock.patch(NIMBLE_RANDOM)
@mock.patch(NIMBLE_URLLIB2)
@mock.patch(NIMBLE_CLIENT)
@NimbleDriverBaseTestCase.client_mock_decorator(create_configuration(
'nimble', 'nimble_pass', '10.18.108.55', 'default', '*'))
def test_initialize_connection_igroup_not_exist(self, mock_random):
mock_random.sample.return_value = 'abcdefghijkl'
self.mock_client_service.service.getInitiatorGrpList.return_value = \
FAKE_IGROUP_LIST_RESPONSE
expected_res = {
'driver_volume_type': 'iscsi',
'data': {
'target_lun': '14',
'volume_id': 12,
'target_iqn': '13',
'target_discovered': False,
'target_portal': '12'}}
self.assertEqual(
expected_res,
self.driver.initialize_connection(
{'name': 'test-volume',
'provider_location': '12 13 14',
'id': 12},
{'initiator': 'test-initiator3'}))
expected_calls = [
mock.call.service.getInitiatorGrpList(
request={'sid': 'a9b9aba7'}),
mock.call.service.createInitiatorGrp(
request={
'attr': {'initiator-list': [{'name': 'test-initiator3',
'label': 'test-initiator3'}],
'name': 'openstack-abcdefghijkl'},
'sid': 'a9b9aba7'}),
mock.call.service.addVolAcl(
request={'volname': 'test-volume', 'apply-to': 3,
'chapuser': '*',
'initiatorgrp': 'openstack-abcdefghijkl',
'sid': 'a9b9aba7'})]
self.mock_client_service.assert_has_calls(
self.mock_client_service.method_calls,
expected_calls)
@mock.patch(NIMBLE_URLLIB2)
@mock.patch(NIMBLE_CLIENT)
@NimbleDriverBaseTestCase.client_mock_decorator(create_configuration(
'nimble', 'nimble_pass', '10.18.108.55', 'default', '*'))
def test_terminate_connection_positive(self):
self.mock_client_service.service.getInitiatorGrpList.return_value = \
FAKE_IGROUP_LIST_RESPONSE
self.driver.terminate_connection(
{'name': 'test-volume',
'provider_location': '12 13 14',
'id': 12},
{'initiator': 'test-initiator1'})
expected_calls = [mock.call.service.getInitiatorGrpList(
request={'sid': 'a9b9aba7'}),
mock.call.service.removeVolAcl(
request={'volname': 'test-volume',
'apply-to': 3,
'chapuser': '*',
'initiatorgrp': {'initiator-list':
[{'name': 'test-initiator1'}]},
'sid': 'a9b9aba7'})]
self.mock_client_service.assert_has_calls(
self.mock_client_service.method_calls,
expected_calls)
@mock.patch(NIMBLE_URLLIB2)
@mock.patch(NIMBLE_CLIENT)
@NimbleDriverBaseTestCase.client_mock_decorator(create_configuration(
'nimble', 'nimble_pass', '10.18.108.55', 'default', '*'))
def test_terminate_connection_negative(self):
self.mock_client_service.service.getInitiatorGrpList.return_value = \
FAKE_IGROUP_LIST_RESPONSE
self.assertRaises(
exception.VolumeDriverException,
self.driver.terminate_connection, {
'name': 'test-volume',
'provider_location': '12 13 14', 'id': 12},
{'initiator': 'test-initiator3'})

View File

@ -0,0 +1,706 @@
# Nimble Storage, Inc. (c) 2013-2014
# 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.
"""
Volume driver for Nimble Storage.
This driver supports Nimble Storage controller CS-Series.
"""
import functools
import random
import re
import string
import urllib2
from oslo.config import cfg
from suds import client
from cinder import exception
from cinder.openstack.common import log as logging
from cinder.openstack.common import units
from cinder.volume.drivers.san.san import SanISCSIDriver
DRIVER_VERSION = '1.0'
VOL_EDIT_MASK = 4 + 16 + 32 + 64 + 512
SOAP_PORT = 5391
SM_ACL_APPLY_TO_BOTH = 3
SM_ACL_CHAP_USER_ANY = '*'
SM_SUBNET_DATA = 3
SM_SUBNET_MGMT_PLUS_DATA = 4
LUN_ID = '0'
WARN_LEVEL = 0.8
LOG = logging.getLogger(__name__)
nimble_opts = [
cfg.StrOpt('nimble_pool_name',
default='default',
help='Nimble Controller pool name'),
cfg.StrOpt('nimble_subnet_label',
default='*',
help='Nimble Subnet Label'), ]
CONF = cfg.CONF
CONF.register_opts(nimble_opts)
class NimbleDriverException(exception.VolumeDriverException):
message = _("Nimble Cinder Driver exception")
class NimbleAPIException(exception.VolumeBackendAPIException):
message = _("Unexpected response from Nimble API")
class NimbleISCSIDriver(SanISCSIDriver):
"""OpenStack driver to enable Nimble Controller.
Version history:
1.0 - Initial driver
"""
def __init__(self, *args, **kwargs):
super(NimbleISCSIDriver, self).__init__(*args, **kwargs)
self.APIExecutor = None
self.group_stats = {}
self.configuration.append_config_values(nimble_opts)
def _check_config(self):
"""Ensure that the flags we care about are set."""
required_config = ['san_ip', 'san_login', 'san_password']
for attr in required_config:
if not getattr(self.configuration, attr, None):
raise exception.InvalidInput(reason=_('%s is not set.') %
attr)
def _get_discovery_ip(self, netconfig):
"""Get discovery ip."""
subnet_label = self.configuration.nimble_subnet_label
LOG.debug('subnet_label used %(netlabel)s, netconfig %(netconf)s'
% {'netlabel': subnet_label, 'netconf': netconfig})
ret_discovery_ip = None
for subnet in netconfig['subnet-list']:
LOG.info(_('Exploring array subnet label %s') % subnet['label'])
if subnet_label == '*':
# Use the first data subnet, save mgmt+data for later
if (subnet['subnet-id']['type'] == SM_SUBNET_DATA):
LOG.info(_('Discovery ip %(disc_ip)s is used '
'on data subnet %(net_label)s')
% {'disc_ip': subnet['discovery-ip'],
'net_label': subnet['label']})
return subnet['discovery-ip']
elif (subnet['subnet-id']['type'] ==
SM_SUBNET_MGMT_PLUS_DATA):
LOG.info(_('Discovery ip %(disc_ip)s is found'
' on mgmt+data subnet %(net_label)s')
% {'disc_ip': subnet['discovery-ip'],
'net_label': subnet['label']})
ret_discovery_ip = subnet['discovery-ip']
# If subnet is specified and found, use the subnet
elif subnet_label == subnet['label']:
LOG.info(_('Discovery ip %(disc_ip)s is used'
' on subnet %(net_label)s')
% {'disc_ip': subnet['discovery-ip'],
'net_label': subnet['label']})
return subnet['discovery-ip']
if ret_discovery_ip:
LOG.info(_('Discovery ip %s is used on mgmt+data subnet')
% ret_discovery_ip)
return ret_discovery_ip
else:
raise NimbleDriverException(_('No suitable discovery ip found'))
def do_setup(self, context):
"""Setup the Nimble Cinder volume driver."""
self._check_config()
# Setup API Executor
try:
self.APIExecutor = NimbleAPIExecutor(
username=self.configuration.san_login,
password=self.configuration.san_password,
ip=self.configuration.san_ip)
except Exception:
LOG.error(_('Failed to create SOAP client.'
'Check san_ip, username, password'
' and make sure the array version is compatible'))
raise
def _get_provider_location(self, volume_name):
"""Get volume iqn for initiator access."""
vol_info = self.APIExecutor.get_vol_info(volume_name)
iqn = vol_info['target-name']
netconfig = self.APIExecutor.get_netconfig('active')
target_ipaddr = self._get_discovery_ip(netconfig)
iscsi_portal = target_ipaddr + ':3260'
provider_location = '%s %s %s' % (iscsi_portal, iqn, LUN_ID)
LOG.info(_('vol_name=%(name)s provider_location=%(loc)s')
% {'name': volume_name, 'loc': provider_location})
return provider_location
def _get_model_info(self, volume_name):
"""Get model info for the volume."""
return (
{'provider_location': self._get_provider_location(volume_name),
'provider_auth': None})
def create_volume(self, volume):
"""Create a new volume."""
reserve = not self.configuration.san_thin_provision
self.APIExecutor.create_vol(
volume,
self.configuration.nimble_pool_name, reserve)
return self._get_model_info(volume['name'])
def delete_volume(self, volume):
"""Delete the specified volume."""
self.APIExecutor.online_vol(volume['name'], False,
ignore_list=['SM-enoent'])
self.APIExecutor.dissociate_volcoll(volume['name'],
ignore_list=['SM-enoent'])
self.APIExecutor.delete_vol(volume['name'], ignore_list=['SM-enoent'])
def _generate_random_string(self, length):
"""Generates random_string."""
char_set = string.ascii_lowercase
return ''.join(random.sample(char_set, length))
def _clone_volume_from_snapshot(self, volume, snapshot):
"""Clonevolume from snapshot. Extend the volume if the
size of the volume is more than the snapshot
"""
reserve = not self.configuration.san_thin_provision
self.APIExecutor.clone_vol(volume, snapshot, reserve)
if(volume['size'] > snapshot['volume_size']):
vol_size = volume['size'] * units.Gi
reserve_size = vol_size if reserve else 0
self.APIExecutor.edit_vol(
volume['name'],
VOL_EDIT_MASK, # mask for vol attributes
{'size': vol_size,
'reserve': reserve_size,
'warn-level': int(vol_size * WARN_LEVEL),
'quota': vol_size,
'snap-quota': vol_size})
return self._get_model_info(volume['name'])
def create_cloned_volume(self, volume, src_vref):
"""Create a clone of the specified volume."""
snapshot_name = ('openstack-clone-' +
volume['name'] + '-' +
self._generate_random_string(12))
snapshot = {'volume_name': src_vref['name'],
'name': snapshot_name,
'volume_size': src_vref['size']}
self.APIExecutor.snap_vol(snapshot)
self._clone_volume_from_snapshot(volume, snapshot)
return self._get_model_info(volume['name'])
def create_export(self, context, volume):
"""Driver entry point to get the export info for a new volume."""
return self._get_model_info(volume['name'])
def ensure_export(self, context, volume):
"""Driver entry point to get the export info for an existing volume."""
return self._get_model_info(volume['name'])
def create_snapshot(self, snapshot):
"""Create a snapshot."""
self.APIExecutor.snap_vol(snapshot)
def delete_snapshot(self, snapshot):
"""Delete a snapshot."""
self.APIExecutor.online_snap(
snapshot['volume_name'],
False,
snapshot['name'],
ignore_list=['SM-ealready', 'SM-enoent'])
self.APIExecutor.delete_snap(snapshot['volume_name'],
snapshot['name'],
ignore_list=['SM-enoent'])
def create_volume_from_snapshot(self, volume, snapshot):
"""Create a volume from a snapshot."""
self._clone_volume_from_snapshot(volume, snapshot)
return self._get_model_info(volume['name'])
def get_volume_stats(self, refresh=False):
"""Get volume stats. This is more of getting group stats."""
if refresh:
group_info = self.APIExecutor.get_group_config()
if not group_info['spaceInfoValid']:
raise NimbleDriverException(_('SpaceInfo returned by'
'array is invalid'))
total_capacity = (group_info['usableCapacity'] /
float(units.Gi))
used_space = ((group_info['volUsageCompressed'] +
group_info['snapUsageCompressed'] +
group_info['unusedReserve']) /
float(units.Gi))
free_space = total_capacity - used_space
LOG.debug('total_capacity=%(capacity)f '
'used_space=%(used)f free_space=%(free)f'
% {'capacity': total_capacity,
'used': used_space,
'free': free_space})
backend_name = self.configuration.safe_get(
'volume_backend_name') or self.__class__.__name__
self.group_stats = {'volume_backend_name': backend_name,
'vendor_name': 'Nimble',
'driver_version': DRIVER_VERSION,
'storage_protocol': 'iSCSI',
'total_capacity_gb': total_capacity,
'free_capacity_gb': free_space,
'reserved_percentage': 0,
'QoS_support': False}
return self.group_stats
def extend_volume(self, volume, new_size):
"""Extend an existing volume."""
volume_name = volume['name']
LOG.info(_('Entering extend_volume volume=%(vol)s new_size=%(size)s')
% {'vol': volume_name, 'size': new_size})
vol_size = int(new_size) * units.Gi
reserve = not self.configuration.san_thin_provision
reserve_size = vol_size if reserve else 0
self.APIExecutor.edit_vol(
volume_name,
VOL_EDIT_MASK, # mask for vol attributes
{'size': vol_size,
'reserve': reserve_size,
'warn-level': int(vol_size * WARN_LEVEL),
'quota': vol_size,
'snap-quota': vol_size})
def _create_igroup_for_initiator(self, initiator_name):
"""Creates igroup for an initiator and returns the igroup name."""
igrp_name = 'openstack-' + self._generate_random_string(12)
LOG.info(_('Creating initiator group %(grp)s with initiator %(iname)s')
% {'grp': igrp_name, 'iname': initiator_name})
self.APIExecutor.create_initiator_group(igrp_name, initiator_name)
return igrp_name
def _get_igroupname_for_initiator(self, initiator_name):
initiator_groups = self.APIExecutor.get_initiator_grp_list()
for initiator_group in initiator_groups:
if 'initiator-list' in initiator_group:
if (len(initiator_group['initiator-list']) == 1 and
initiator_group['initiator-list'][0]['name'] ==
initiator_name):
LOG.info(_('igroup %(grp)s found for initiator %(iname)s')
% {'grp': initiator_group['name'],
'iname': initiator_name})
return initiator_group['name']
LOG.info(_('No igroup found for initiator %s') % initiator_name)
return None
def initialize_connection(self, volume, connector):
"""Driver entry point to attach a volume to an instance."""
LOG.info(_('Entering initialize_connection volume=%(vol)s'
' connector=%(conn)s location=%(loc)s')
% {'vol': volume,
'conn': connector,
'loc': volume['provider_location']})
initiator_name = connector['initiator']
initiator_group_name = self._get_igroupname_for_initiator(
initiator_name)
if not initiator_group_name:
initiator_group_name = self._create_igroup_for_initiator(
initiator_name)
LOG.info(_('Initiator group name is %(grp)s for initiator %(iname)s')
% {'grp': initiator_group_name, 'iname': initiator_name})
self.APIExecutor.add_acl(volume, initiator_group_name)
(iscsi_portal, iqn, lun_num) = volume['provider_location'].split()
properties = {}
properties['target_discovered'] = False # whether discovery was used
properties['target_portal'] = iscsi_portal
properties['target_iqn'] = iqn
properties['target_lun'] = lun_num
properties['volume_id'] = volume['id'] # used by xen currently
return {
'driver_volume_type': 'iscsi',
'data': properties,
}
def terminate_connection(self, volume, connector, **kwargs):
"""Driver entry point to unattach a volume from an instance."""
LOG.info(_('Entering terminate_connection volume=%(vol)s'
' connector=%(conn)s location=%(loc)s.')
% {'vol': volume,
'conn': connector,
'loc': volume['provider_location']})
initiator_name = connector['initiator']
initiator_group_name = self._get_igroupname_for_initiator(
initiator_name)
if not initiator_group_name:
raise NimbleDriverException(
_('No initiator group found for initiator %s') %
initiator_name)
self.APIExecutor.remove_acl(volume, initiator_group_name)
def _response_checker(func):
"""Decorator function to check if the response
of an API is positive
"""
@functools.wraps(func)
def inner_response_checker(self, *args, **kwargs):
response = func(self, *args, **kwargs)
ignore_list = (kwargs['ignore_list']
if 'ignore_list' in kwargs else [])
for err in response['err-list']['err-list']:
err_str = self._get_err_str(err['code'])
if err_str != 'SM-ok' and err_str not in ignore_list:
msg = (_('API %(name)s failed with error string %(err)s')
% {'name': func.__name__, 'err': err_str})
LOG.error(msg)
raise NimbleAPIException(msg)
return response
return inner_response_checker
def _connection_checker(func):
"""Decorator to re-establish and
re-run the api if session has expired.
"""
@functools.wraps(func)
def inner_connection_checker(self, *args, **kwargs):
for attempts in range(2):
try:
return func(self, *args, **kwargs)
except NimbleAPIException as e:
if attempts < 1 and (re.search('SM-eaccess', str(e))):
LOG.info(_('Session might have expired.'
' Trying to relogin'))
self.login()
continue
else:
LOG.error(_('Re-throwing Exception %s') % e)
raise
return inner_connection_checker
class NimbleAPIExecutor:
"""Makes Nimble API calls."""
def __init__(self, *args, **kwargs):
self.sid = None
self.username = kwargs['username']
self.password = kwargs['password']
wsdl_url = 'https://%s/wsdl/NsGroupManagement.wsdl' % (kwargs['ip'])
LOG.debug('Using Nimble wsdl_url: %s' % wsdl_url)
self.err_string_dict = self._create_err_code_to_str_mapper(wsdl_url)
self.client = client.Client(wsdl_url,
username=self.username,
password=self.password)
soap_url = ('https://%(ip)s:%(port)s/soap' % {'ip': kwargs['ip'],
'port': SOAP_PORT})
LOG.debug('Using Nimble soap_url: %s' % soap_url)
self.client.set_options(location=soap_url)
self.login()
def _create_err_code_to_str_mapper(self, wsdl_url):
f = urllib2.urlopen(wsdl_url)
wsdl_file = f.read()
err_enums = re.findall(
r'<simpleType name="SmErrorType">(.*?)</simpleType>',
wsdl_file,
re.DOTALL)
err_enums = ''.join(err_enums).split('\n')
ret_dict = {}
for enum in err_enums:
m = re.search(r'"(.*?)"(.*?)= (\d+) ', enum)
if m:
ret_dict[int(m.group(3))] = m.group(1)
return ret_dict
def _get_err_str(self, code):
if code in self.err_string_dict:
return self.err_string_dict[code]
else:
return 'Unknown error Code: %s' % code
@_response_checker
def _execute_login(self):
return self.client.service.login(req={
'username': self.username,
'password': self.password
})
def login(self):
"""Execute Https Login API."""
response = self._execute_login()
LOG.info(_('Successful login by user %s') % self.username)
self.sid = response['authInfo']['sid']
@_connection_checker
@_response_checker
def _execute_get_netconfig(self, name):
return self.client.service.getNetConfig(request={'sid': self.sid,
'name': name})
def get_netconfig(self, name):
"""Execute getNetConfig API."""
response = self._execute_get_netconfig(name)
return response['config']
@_connection_checker
@_response_checker
def _execute_create_vol(self, volume, pool_name, reserve):
# Set volume size, display name and description
volume_size = volume['size'] * units.Gi
reserve_size = volume_size if reserve else 0
display_name = (volume['display_name']
if 'display_name' in volume else '')
display_description = (': ' + volume['display_description']
if 'display_description' in volume else '')
description = display_name + display_description
# Limit description size to 254 characters
description = description[:254]
LOG.info(_('Creating a new volume=%(vol)s size=%(size)s'
' reserve=%(reserve)s in pool=%(pool)s')
% {'vol': volume['name'],
'size': volume_size,
'reserve': reserve,
'pool': pool_name})
return self.client.service.createVol(
request={'sid': self.sid,
'attr': {'name': volume['name'],
'description': description,
'size': volume_size,
'perfpol-name': 'default',
'reserve': reserve_size,
'warn-level': int(volume_size * WARN_LEVEL),
'quota': volume_size,
'snap-quota': volume_size,
'online': True,
'pool-name': pool_name}})
def create_vol(self, volume, pool_name, reserve):
"""Execute createVol API."""
response = self._execute_create_vol(volume, pool_name, reserve)
LOG.info(_('Successfuly create volume %s') % response['name'])
return response['name']
@_connection_checker
@_response_checker
def _execute_get_group_config(self):
LOG.debug('Getting group config information')
return self.client.service.getGroupConfig(request={'sid': self.sid})
def get_group_config(self):
"""Execute getGroupConfig API."""
response = self._execute_get_group_config()
LOG.debug('Successfuly retrieved group config information')
return response['info']
@_connection_checker
@_response_checker
def add_acl(self, volume, initiator_group_name):
"""Execute addAcl API."""
LOG.info(_('Adding ACL to volume=%(vol)s with'
' initiator group name %(igrp)s')
% {'vol': volume['name'],
'igrp': initiator_group_name})
return self.client.service.addVolAcl(
request={'sid': self.sid,
'volname': volume['name'],
'apply-to': SM_ACL_APPLY_TO_BOTH,
'chapuser': SM_ACL_CHAP_USER_ANY,
'initiatorgrp': initiator_group_name})
@_connection_checker
@_response_checker
def remove_acl(self, volume, initiator_group_name):
"""Execute removeVolAcl API."""
LOG.info(_('Removing ACL from volume=%(vol)s'
' for initiator group %(igrp)s')
% {'vol': volume['name'],
'igrp': initiator_group_name})
return self.client.service.removeVolAcl(
request={'sid': self.sid,
'volname': volume['name'],
'apply-to': SM_ACL_APPLY_TO_BOTH,
'chapuser': SM_ACL_CHAP_USER_ANY,
'initiatorgrp': initiator_group_name})
@_connection_checker
@_response_checker
def _execute_get_vol_info(self, vol_name):
LOG.info(_('Getting volume information for vol_name=%s') % (vol_name))
return self.client.service.getVolInfo(request={'sid': self.sid,
'name': vol_name})
def get_vol_info(self, vol_name):
"""Execute getVolInfo API."""
response = self._execute_get_vol_info(vol_name)
LOG.info(_('Successfuly got volume information for volume %s')
% vol_name)
return response['vol']
@_connection_checker
@_response_checker
def online_vol(self, vol_name, online_flag, *args, **kwargs):
"""Execute onlineVol API."""
LOG.info(_('Setting volume %(vol)s to online_flag %(flag)s')
% {'vol': vol_name, 'flag': online_flag})
return self.client.service.onlineVol(request={'sid': self.sid,
'name': vol_name,
'online': online_flag})
@_connection_checker
@_response_checker
def online_snap(self, vol_name, online_flag, snap_name, *args, **kwargs):
"""Execute onlineSnap API."""
LOG.info(_('Setting snapshot %(snap)s to online_flag %(flag)s')
% {'snap': snap_name, 'flag': online_flag})
return self.client.service.onlineSnap(request={'sid': self.sid,
'vol': vol_name,
'name': snap_name,
'online': online_flag})
@_connection_checker
@_response_checker
def dissociate_volcoll(self, vol_name, *args, **kwargs):
"""Execute dissocProtPol API."""
LOG.info(_('Dissociating volume %s ') % vol_name)
return self.client.service.dissocProtPol(
request={'sid': self.sid,
'vol-name': vol_name})
@_connection_checker
@_response_checker
def delete_vol(self, vol_name, *args, **kwargs):
"""Execute deleteVol API."""
LOG.info(_('Deleting volume %s ') % vol_name)
return self.client.service.deleteVol(request={'sid': self.sid,
'name': vol_name})
@_connection_checker
@_response_checker
def snap_vol(self, snapshot):
"""Execute snapVol API."""
volume_name = snapshot['volume_name']
snap_name = snapshot['name']
# Set description
snap_display_name = (snapshot['display_name']
if 'display_name' in snapshot else '')
snap_display_description = (
': ' + snapshot['display_description']
if 'display_description' in snapshot else '')
snap_description = snap_display_name + snap_display_description
# Limit to 254 characters
snap_description = snap_description[:254]
LOG.info(_('Creating snapshot for volume_name=%(vol)s'
' snap_name=%(name)s snap_description=%(desc)s')
% {'vol': volume_name,
'name': snap_name,
'desc': snap_description})
return self.client.service.snapVol(
request={'sid': self.sid,
'vol': volume_name,
'snapAttr': {'name': snap_name,
'description': snap_description}})
@_connection_checker
@_response_checker
def delete_snap(self, vol_name, snap_name, *args, **kwargs):
"""Execute deleteSnap API."""
LOG.info(_('Deleting snapshot %s ') % snap_name)
return self.client.service.deleteSnap(request={'sid': self.sid,
'vol': vol_name,
'name': snap_name})
@_connection_checker
@_response_checker
def clone_vol(self, volume, snapshot, reserve):
"""Execute cloneVol API."""
volume_name = snapshot['volume_name']
snap_name = snapshot['name']
clone_name = volume['name']
snap_size = snapshot['volume_size']
reserve_size = snap_size * units.Gi if reserve else 0
LOG.info(_('Cloning volume from snapshot volume=%(vol)s '
'snapshot=%(snap)s clone=%(clone)s snap_size=%(size)s'
'reserve=%(reserve)s')
% {'vol': volume_name,
'snap': snap_name,
'clone': clone_name,
'size': snap_size,
'reserve': reserve})
clone_size = snap_size * units.Gi
return self.client.service.cloneVol(
request={'sid': self.sid,
'name': volume_name,
'attr': {'name': clone_name,
'perfpol-name': 'default',
'reserve': reserve_size,
'warn-level': int(clone_size * WARN_LEVEL),
'quota': clone_size,
'snap-quota': clone_size,
'online': True},
'snap-name': snap_name})
@_connection_checker
@_response_checker
def edit_vol(self, vol_name, mask, attr):
"""Execute editVol API."""
LOG.info(_('Editing Volume %(vol)s with mask %(mask)s')
% {'vol': vol_name, 'mask': str(mask)})
return self.client.service.editVol(request={'sid': self.sid,
'name': vol_name,
'mask': mask,
'attr': attr})
@_connection_checker
@_response_checker
def _execute_get_initiator_grp_list(self):
LOG.info(_('Getting getInitiatorGrpList'))
return (self.client.service.getInitiatorGrpList(
request={'sid': self.sid}))
def get_initiator_grp_list(self):
"""Execute getInitiatorGrpList API."""
response = self._execute_get_initiator_grp_list()
LOG.info(_('Successfuly retrieved InitiatorGrpList'))
return (response['initiatorgrp-list']
if 'initiatorgrp-list' in response else [])
@_connection_checker
@_response_checker
def create_initiator_group(self, initiator_group_name, initiator_name):
"""Execute createInitiatorGrp API."""
LOG.info(_('Creating initiator group %(igrp)s'
' with one initiator %(iname)s')
% {'igrp': initiator_group_name, 'iname': initiator_name})
return self.client.service.createInitiatorGrp(
request={'sid': self.sid,
'attr': {'name': initiator_group_name,
'initiator-list': [{'label': initiator_name,
'name': initiator_name}]}})
@_connection_checker
@_response_checker
def delete_initiator_group(self, initiator_group_name, *args, **kwargs):
"""Execute deleteInitiatorGrp API."""
LOG.info(_('Deleting deleteInitiatorGrp %s ') % initiator_group_name)
return self.client.service.deleteInitiatorGrp(
request={'sid': self.sid,
'name': initiator_group_name})

View File

@ -1547,6 +1547,17 @@
#nfs_mount_options=<None>
#
# Options defined in cinder.volume.drivers.nimble
#
# Nimble Controller pool name (string value)
#nimble_pool_name=default
# Nimble Subnet Label (string value)
#nimble_subnet_label=*
#
# Options defined in cinder.volume.drivers.rbd
#