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:
parent
350334dec0
commit
62f5e2803a
554
cinder/tests/test_nimble.py
Normal file
554
cinder/tests/test_nimble.py
Normal 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'})
|
706
cinder/volume/drivers/nimble.py
Normal file
706
cinder/volume/drivers/nimble.py
Normal 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})
|
@ -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
|
||||
#
|
||||
|
Loading…
Reference in New Issue
Block a user