--- /dev/null
+# 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'})
--- /dev/null
+# 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})