Adds Unity Cinder Driver
This driver supports both FC and iSCSI protocols and supports below operations: - Create / Delete volume - Extend volume - Attach / Detach volume - Create / Delete snapshot - Copy Image to Volume - Copy Volume to Image - Clone volume - Create volume from snapshot - Manage / Unmanage volume DocImpact Co-Authored-By: Cedric Zhuang <cedric.zhuang@emc.com> Co-Authored-By: Ryan Liang <ryan.liang@emc.com> Implements: blueprint emc-unity-driver Change-Id: I9573a9704342d77e6e5ce5746b8f29c6246af527
This commit is contained in:
parent
a6b5a19e0d
commit
5a8f26eb62
@ -74,6 +74,8 @@ from cinder.volume.drivers.coprhd import scaleio as \
|
||||
from cinder.volume.drivers import datera as cinder_volume_drivers_datera
|
||||
from cinder.volume.drivers.dell import dell_storagecenter_common as \
|
||||
cinder_volume_drivers_dell_dellstoragecentercommon
|
||||
from cinder.volume.drivers.dell_emc.unity import driver as \
|
||||
cinder_volume_drivers_dell_emc_unity_driver
|
||||
from cinder.volume.drivers.disco import disco as \
|
||||
cinder_volume_drivers_disco_disco
|
||||
from cinder.volume.drivers.dothill import dothill_common as \
|
||||
@ -264,6 +266,7 @@ def list_opts():
|
||||
cinder_volume_drivers_datera.d_opts,
|
||||
cinder_volume_drivers_dell_dellstoragecentercommon.
|
||||
common_opts,
|
||||
cinder_volume_drivers_dell_emc_unity_driver.UNITY_OPTS,
|
||||
cinder_volume_drivers_disco_disco.disco_opts,
|
||||
cinder_volume_drivers_dothill_dothillcommon.common_opts,
|
||||
cinder_volume_drivers_dothill_dothillcommon.iscsi_opts,
|
||||
|
@ -0,0 +1,70 @@
|
||||
# Copyright (c) 2016 Dell Inc. or its subsidiaries.
|
||||
# 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.
|
||||
|
||||
|
||||
class StoropsException(Exception):
|
||||
message = 'Storops Error.'
|
||||
|
||||
|
||||
class UnityLunNameInUseError(StoropsException):
|
||||
pass
|
||||
|
||||
|
||||
class UnityResourceNotFoundError(StoropsException):
|
||||
pass
|
||||
|
||||
|
||||
class UnitySnapNameInUseError(StoropsException):
|
||||
pass
|
||||
|
||||
|
||||
class UnityDeleteAttachedSnapError(StoropsException):
|
||||
pass
|
||||
|
||||
|
||||
class UnityResourceAlreadyAttachedError(StoropsException):
|
||||
pass
|
||||
|
||||
|
||||
class UnityPolicyNameInUseError(StoropsException):
|
||||
pass
|
||||
|
||||
|
||||
class UnityNothingToModifyError(StoropsException):
|
||||
pass
|
||||
|
||||
|
||||
class ExtendLunError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class DetachIsCalled(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class LunDeleteIsCalled(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class SnapDeleteIsCalled(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class UnexpectedLunDeletion(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class AdapterSetupError(Exception):
|
||||
pass
|
516
cinder/tests/unit/volume/drivers/dell_emc/unity/test_adapter.py
Normal file
516
cinder/tests/unit/volume/drivers/dell_emc/unity/test_adapter.py
Normal file
@ -0,0 +1,516 @@
|
||||
# Copyright (c) 2016 Dell Inc. or its subsidiaries.
|
||||
# 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 functools
|
||||
import unittest
|
||||
|
||||
import mock
|
||||
|
||||
from cinder import exception
|
||||
from cinder.tests.unit.volume.drivers.dell_emc.unity \
|
||||
import fake_exception as ex
|
||||
from cinder.tests.unit.volume.drivers.dell_emc.unity import test_client
|
||||
from cinder.volume.drivers.dell_emc.unity import adapter
|
||||
|
||||
|
||||
########################
|
||||
#
|
||||
# Start of Mocks
|
||||
#
|
||||
########################
|
||||
class MockConfig(object):
|
||||
def __init__(self):
|
||||
self.unity_storage_pool_names = ['pool1', 'pool2']
|
||||
self.reserved_percentage = 5
|
||||
self.max_over_subscription_ratio = 300
|
||||
self.volume_backend_name = 'backend'
|
||||
self.san_ip = '1.2.3.4'
|
||||
self.san_login = 'user'
|
||||
self.san_password = 'pass'
|
||||
self.driver_ssl_cert_verify = False
|
||||
self.driver_ssl_cert_path = None
|
||||
|
||||
def safe_get(self, name):
|
||||
return getattr(self, name)
|
||||
|
||||
|
||||
class MockConnector(object):
|
||||
@staticmethod
|
||||
def disconnect_volume(data, device):
|
||||
pass
|
||||
|
||||
|
||||
class MockDriver(object):
|
||||
def __init__(self):
|
||||
self.configuration = mock.Mock(volume_dd_blocksize='1M')
|
||||
|
||||
@staticmethod
|
||||
def _connect_device(conn):
|
||||
return {'connector': MockConnector(),
|
||||
'device': {'path': 'dev'},
|
||||
'conn': {'data': {}}}
|
||||
|
||||
|
||||
class MockClient(object):
|
||||
@staticmethod
|
||||
def get_pools():
|
||||
return test_client.MockResourceList(['pool0', 'pool1'])
|
||||
|
||||
@staticmethod
|
||||
def create_lun(name, size, pool, description=None, io_limit_policy=None):
|
||||
return test_client.MockResource(_id='lun_3')
|
||||
|
||||
@staticmethod
|
||||
def get_lun(name=None, lun_id=None):
|
||||
if lun_id is None:
|
||||
lun_id = 'lun_4'
|
||||
if name == 'not_exists':
|
||||
ret = test_client.MockResource(name=lun_id)
|
||||
ret.existed = False
|
||||
else:
|
||||
ret = test_client.MockResource(_id=lun_id)
|
||||
return ret
|
||||
|
||||
@staticmethod
|
||||
def delete_lun(lun_id):
|
||||
if lun_id != 'lun_4':
|
||||
raise ex.UnexpectedLunDeletion()
|
||||
|
||||
@staticmethod
|
||||
def get_serial():
|
||||
return 'CLIENT_SERIAL'
|
||||
|
||||
@staticmethod
|
||||
def create_snap(src_lun_id, name=None):
|
||||
return test_client.MockResource(name=name, _id=src_lun_id)
|
||||
|
||||
@staticmethod
|
||||
def get_snap(name=None):
|
||||
snap = test_client.MockResource(name=name)
|
||||
if name is not None:
|
||||
ret = snap
|
||||
else:
|
||||
ret = [snap]
|
||||
return ret
|
||||
|
||||
@staticmethod
|
||||
def delete_snap(snap):
|
||||
if snap.name in ('abc-def_snap',):
|
||||
raise ex.SnapDeleteIsCalled()
|
||||
|
||||
@staticmethod
|
||||
def create_host(name, uids):
|
||||
return test_client.MockResource(name=name)
|
||||
|
||||
@staticmethod
|
||||
def get_host(name):
|
||||
return test_client.MockResource(name=name)
|
||||
|
||||
@staticmethod
|
||||
def attach(host, lun_or_snap):
|
||||
return 10
|
||||
|
||||
@staticmethod
|
||||
def detach(host, lun_or_snap):
|
||||
error_ids = ['lun_43', 'snap_0']
|
||||
if host.name == 'host1' and lun_or_snap.get_id() in error_ids:
|
||||
raise ex.DetachIsCalled()
|
||||
|
||||
@staticmethod
|
||||
def get_iscsi_target_info():
|
||||
return [{'portal': '1.2.3.4:1234', 'iqn': 'iqn.1-1.com.e:c.a.a0'},
|
||||
{'portal': '1.2.3.5:1234', 'iqn': 'iqn.1-1.com.e:c.a.a1'}]
|
||||
|
||||
@staticmethod
|
||||
def get_fc_target_info(host=None, logged_in_only=False):
|
||||
if host and host.name == 'no_target':
|
||||
ret = []
|
||||
else:
|
||||
ret = ['8899AABBCCDDEEFF', '8899AABBCCDDFFEE']
|
||||
return ret
|
||||
|
||||
@staticmethod
|
||||
def create_lookup_service():
|
||||
return {}
|
||||
|
||||
@staticmethod
|
||||
def get_io_limit_policy(specs):
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def extend_lun(lun_id, size_gib):
|
||||
if size_gib <= 0:
|
||||
raise ex.ExtendLunError
|
||||
|
||||
|
||||
class MockLookupService(object):
|
||||
@staticmethod
|
||||
def get_device_mapping_from_network(initiator_wwns, target_wwns):
|
||||
return {
|
||||
'san_1': {
|
||||
'initiator_port_wwn_list':
|
||||
('200000051e55a100', '200000051e55a121'),
|
||||
'target_port_wwn_list':
|
||||
('100000051e55a100', '100000051e55a121')
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
def mock_adapter(driver_clz):
|
||||
ret = driver_clz()
|
||||
ret._client = MockClient()
|
||||
ret.do_setup(MockDriver(), MockConfig())
|
||||
ret.lookup_service = MockLookupService()
|
||||
return ret
|
||||
|
||||
|
||||
def get_backend_qos_specs(volume):
|
||||
return None
|
||||
|
||||
|
||||
def get_connector_properties():
|
||||
return {'host': 'host1', 'wwpns': 'abcdefg'}
|
||||
|
||||
|
||||
def copy_volume(from_path, to_path, size_in_m, block_size, sparse=True):
|
||||
pass
|
||||
|
||||
|
||||
def get_lun_pl(name):
|
||||
return 'id^%s|system^CLIENT_SERIAL|type^lun|version^None' % name
|
||||
|
||||
|
||||
def patch_for_unity_adapter(func):
|
||||
@functools.wraps(func)
|
||||
@mock.patch('cinder.volume.drivers.dell_emc.unity.utils.'
|
||||
'get_backend_qos_specs',
|
||||
new=get_backend_qos_specs)
|
||||
@mock.patch('cinder.utils.brick_get_connector_properties',
|
||||
new=get_connector_properties)
|
||||
@mock.patch('cinder.volume.utils.copy_volume', new=copy_volume)
|
||||
def func_wrapper(*args, **kwargs):
|
||||
return func(*args, **kwargs)
|
||||
|
||||
return func_wrapper
|
||||
|
||||
|
||||
########################
|
||||
#
|
||||
# Start of Tests
|
||||
#
|
||||
########################
|
||||
class CommonAdapterTest(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.adapter = mock_adapter(adapter.CommonAdapter)
|
||||
|
||||
def test_get_managed_pools(self):
|
||||
ret = self.adapter.get_managed_pools()
|
||||
self.assertIn('pool1', ret)
|
||||
self.assertNotIn('pool0', ret)
|
||||
self.assertNotIn('pool2', ret)
|
||||
|
||||
@patch_for_unity_adapter
|
||||
def test_create_volume(self):
|
||||
volume = mock.Mock(size=5, host='unity#pool1')
|
||||
ret = self.adapter.create_volume(volume)
|
||||
expected = get_lun_pl('lun_3')
|
||||
self.assertEqual(expected, ret['provider_location'])
|
||||
|
||||
def test_create_snapshot(self):
|
||||
volume = mock.Mock(provider_location='id^lun_43')
|
||||
snap = mock.Mock(volume=volume)
|
||||
snap.name = 'abc-def_snap'
|
||||
result = self.adapter.create_snapshot(snap)
|
||||
self.assertEqual('abc-def_snap', result.name)
|
||||
self.assertEqual('lun_43', result.get_id())
|
||||
|
||||
def test_delete_snap(self):
|
||||
def f():
|
||||
snap = mock.Mock()
|
||||
snap.name = 'abc-def_snap'
|
||||
|
||||
self.adapter.delete_snapshot(snap)
|
||||
|
||||
self.assertRaises(ex.SnapDeleteIsCalled, f)
|
||||
|
||||
def test_get_lun_id_has_location(self):
|
||||
volume = mock.Mock(provider_location='id^lun_43')
|
||||
self.assertEqual('lun_43', self.adapter.get_lun_id(volume))
|
||||
|
||||
def test_get_lun_id_no_location(self):
|
||||
volume = mock.Mock(provider_location=None)
|
||||
self.assertEqual('lun_4', self.adapter.get_lun_id(volume))
|
||||
|
||||
def test_delete_volume(self):
|
||||
volume = mock.Mock(provider_location='id^lun_4')
|
||||
self.adapter.delete_volume(volume)
|
||||
|
||||
def test_get_pool_stats(self):
|
||||
stats_list = self.adapter.get_pools_stats()
|
||||
self.assertEqual(1, len(stats_list))
|
||||
|
||||
stats = stats_list[0]
|
||||
self.assertEqual('pool1', stats['pool_name'])
|
||||
self.assertEqual(5, stats['total_capacity_gb'])
|
||||
self.assertEqual('pool1|CLIENT_SERIAL', stats['location_info'])
|
||||
self.assertEqual(6, stats['provisioned_capacity_gb'])
|
||||
self.assertEqual(2, stats['free_capacity_gb'])
|
||||
self.assertEqual(300, stats['max_over_subscription_ratio'])
|
||||
self.assertEqual(5, stats['reserved_percentage'])
|
||||
self.assertFalse(stats['thick_provisioning_support'])
|
||||
self.assertTrue(stats['thin_provisioning_support'])
|
||||
|
||||
def test_update_volume_stats(self):
|
||||
stats = self.adapter.update_volume_stats()
|
||||
self.assertEqual('backend', stats['volume_backend_name'])
|
||||
self.assertEqual('unknown', stats['storage_protocol'])
|
||||
self.assertTrue(stats['thin_provisioning_support'])
|
||||
self.assertFalse(stats['thick_provisioning_support'])
|
||||
self.assertEqual(1, len(stats['pools']))
|
||||
|
||||
def test_serial_number(self):
|
||||
self.assertEqual('CLIENT_SERIAL', self.adapter.serial_number)
|
||||
|
||||
def test_do_setup(self):
|
||||
self.assertEqual('1.2.3.4', self.adapter.ip)
|
||||
self.assertEqual('user', self.adapter.username)
|
||||
self.assertEqual('pass', self.adapter.password)
|
||||
self.assertFalse(self.adapter.array_cert_verify)
|
||||
self.assertIsNone(self.adapter.array_ca_cert_path)
|
||||
|
||||
def test_verify_cert_false_path_none(self):
|
||||
self.adapter.array_cert_verify = False
|
||||
self.adapter.array_ca_cert_path = None
|
||||
self.assertFalse(self.adapter.verify_cert)
|
||||
|
||||
def test_verify_cert_false_path_not_none(self):
|
||||
self.adapter.array_cert_verify = False
|
||||
self.adapter.array_ca_cert_path = '/tmp/array_ca.crt'
|
||||
self.assertFalse(self.adapter.verify_cert)
|
||||
|
||||
def test_verify_cert_true_path_none(self):
|
||||
self.adapter.array_cert_verify = True
|
||||
self.adapter.array_ca_cert_path = None
|
||||
self.assertTrue(self.adapter.verify_cert)
|
||||
|
||||
def test_verify_cert_true_path_valide(self):
|
||||
self.adapter.array_cert_verify = True
|
||||
self.adapter.array_ca_cert_path = '/tmp/array_ca.crt'
|
||||
self.assertEqual(self.adapter.array_ca_cert_path,
|
||||
self.adapter.verify_cert)
|
||||
|
||||
def test_initialize_connection_common(self):
|
||||
volume = mock.Mock(provider_location='id^lun_43', id='id_43')
|
||||
connector = {'host': 'host1'}
|
||||
data = self.adapter.initialize_connection(volume, connector)['data']
|
||||
self.assertTrue(data['target_discovered'])
|
||||
self.assertEqual('id_43', data['volume_id'])
|
||||
|
||||
def test_initialize_connection_for_resource(self):
|
||||
snap = test_client.MockResource(_id='snap_1')
|
||||
connector = {'host': 'host1'}
|
||||
data = self.adapter._initialize_connection(
|
||||
snap, connector, 'snap_1')['data']
|
||||
self.assertTrue(data['target_discovered'])
|
||||
self.assertEqual('snap_1', data['volume_id'])
|
||||
|
||||
def test_terminate_connection_common(self):
|
||||
def f():
|
||||
volume = mock.Mock(provider_location='id^lun_43', id='id_43')
|
||||
connector = {'host': 'host1'}
|
||||
self.adapter.terminate_connection(volume, connector)
|
||||
|
||||
self.assertRaises(ex.DetachIsCalled, f)
|
||||
|
||||
def test_terminate_connection_snap(self):
|
||||
def f():
|
||||
connector = {'host': 'host1'}
|
||||
snap = test_client.MockResource(_id='snap_0')
|
||||
self.adapter._terminate_connection(snap, connector)
|
||||
|
||||
self.assertRaises(ex.DetachIsCalled, f)
|
||||
|
||||
def test_manage_existing_by_name(self):
|
||||
ref = {'source-id': 12}
|
||||
volume = mock.Mock(name='lun1')
|
||||
ret = self.adapter.manage_existing(volume, ref)
|
||||
expected = get_lun_pl('12')
|
||||
self.assertEqual(expected, ret['provider_location'])
|
||||
|
||||
def test_manage_existing_by_id(self):
|
||||
ref = {'source-name': 'lunx'}
|
||||
volume = mock.Mock(name='lun1')
|
||||
ret = self.adapter.manage_existing(volume, ref)
|
||||
expected = get_lun_pl('lun_4')
|
||||
self.assertEqual(expected, ret['provider_location'])
|
||||
|
||||
def test_manage_existing_invalid_ref(self):
|
||||
def f():
|
||||
ref = {}
|
||||
volume = mock.Mock(name='lun1')
|
||||
self.adapter.manage_existing(volume, ref)
|
||||
|
||||
self.assertRaises(exception.ManageExistingInvalidReference, f)
|
||||
|
||||
def test_manage_existing_lun_not_found(self):
|
||||
def f():
|
||||
ref = {'source-name': 'not_exists'}
|
||||
volume = mock.Mock(name='lun1')
|
||||
self.adapter.manage_existing(volume, ref)
|
||||
|
||||
self.assertRaises(exception.ManageExistingInvalidReference, f)
|
||||
|
||||
@patch_for_unity_adapter
|
||||
def test_manage_existing_get_size_invalid_backend(self):
|
||||
def f():
|
||||
volume = mock.Mock(volume_type_id='thin',
|
||||
host='host@backend#pool1')
|
||||
ref = {'source-id': 12}
|
||||
self.adapter.manage_existing_get_size(volume, ref)
|
||||
|
||||
self.assertRaises(exception.ManageExistingInvalidReference, f)
|
||||
|
||||
@patch_for_unity_adapter
|
||||
def test_manage_existing_get_size_success(self):
|
||||
volume = mock.Mock(volume_type_id='thin', host='host@backend#pool0')
|
||||
ref = {'source-id': 12}
|
||||
volume_size = self.adapter.manage_existing_get_size(volume, ref)
|
||||
self.assertEqual(5, volume_size)
|
||||
|
||||
@patch_for_unity_adapter
|
||||
def test_create_volume_from_snapshot(self):
|
||||
volume = mock.Mock(id='id_44', host='unity#pool1',
|
||||
provider_location=get_lun_pl('12'))
|
||||
snap = mock.Mock(name='snap_44')
|
||||
ret = self.adapter.create_volume_from_snapshot(volume, snap)
|
||||
self.assertEqual(get_lun_pl('lun_3'), ret['provider_location'])
|
||||
|
||||
@patch_for_unity_adapter
|
||||
def test_create_cloned_volume(self):
|
||||
volume = mock.Mock(id='id_55', host='unity#pool1', size=3,
|
||||
provider_location=get_lun_pl('lun55'))
|
||||
src_vref = mock.Mock(id='id_66', name='LUN 66',
|
||||
provider_location=get_lun_pl('lun66'))
|
||||
ret = self.adapter.create_cloned_volume(volume, src_vref)
|
||||
self.assertEqual(get_lun_pl('lun_3'), ret['provider_location'])
|
||||
|
||||
def test_extend_volume_error(self):
|
||||
def f():
|
||||
volume = mock.Mock(id='l56', provider_location=get_lun_pl('lun56'))
|
||||
self.adapter.extend_volume(volume, -1)
|
||||
|
||||
self.assertRaises(ex.ExtendLunError, f)
|
||||
|
||||
def test_extend_volume_no_id(self):
|
||||
def f():
|
||||
volume = mock.Mock(provider_location='type^lun')
|
||||
self.adapter.extend_volume(volume, 5)
|
||||
|
||||
self.assertRaises(exception.VolumeBackendAPIException, f)
|
||||
|
||||
|
||||
class FCAdapterTest(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.adapter = mock_adapter(adapter.FCAdapter)
|
||||
|
||||
def test_setup(self):
|
||||
self.assertIsNotNone(self.adapter.lookup_service)
|
||||
|
||||
def test_auto_zone_enabled(self):
|
||||
self.assertTrue(self.adapter.auto_zone_enabled)
|
||||
|
||||
def test_fc_protocol(self):
|
||||
stats = mock_adapter(adapter.FCAdapter).update_volume_stats()
|
||||
self.assertEqual('FC', stats['storage_protocol'])
|
||||
|
||||
def test_get_connector_uids(self):
|
||||
connector = {'host': 'fake_host',
|
||||
'wwnns': ['1111111111111111',
|
||||
'2222222222222222'],
|
||||
'wwpns': ['3333333333333333',
|
||||
'4444444444444444']
|
||||
}
|
||||
expected = ['11:11:11:11:11:11:11:11:33:33:33:33:33:33:33:33',
|
||||
'22:22:22:22:22:22:22:22:44:44:44:44:44:44:44:44']
|
||||
ret = self.adapter.get_connector_uids(connector)
|
||||
self.assertListEqual(expected, ret)
|
||||
|
||||
def test_get_connection_info_no_targets(self):
|
||||
def f():
|
||||
host = test_client.MockResource('no_target')
|
||||
self.adapter.get_connection_info(12, host, {})
|
||||
|
||||
self.assertRaises(exception.VolumeBackendAPIException, f)
|
||||
|
||||
def test_get_connection_info_auto_zone_enabled(self):
|
||||
host = test_client.MockResource('host1')
|
||||
connector = {'wwpns': 'abcdefg'}
|
||||
ret = self.adapter.get_connection_info(10, host, connector)
|
||||
target_wwns = ['100000051e55a100', '100000051e55a121']
|
||||
self.assertListEqual(target_wwns, ret['target_wwn'])
|
||||
init_target_map = {
|
||||
'200000051e55a100': ('100000051e55a100', '100000051e55a121'),
|
||||
'200000051e55a121': ('100000051e55a100', '100000051e55a121')}
|
||||
self.assertDictEqual(init_target_map, ret['initiator_target_map'])
|
||||
self.assertEqual(10, ret['target_lun'])
|
||||
|
||||
def test_get_connection_info_auto_zone_disabled(self):
|
||||
self.adapter.lookup_service = None
|
||||
host = test_client.MockResource('host1')
|
||||
connector = {'wwpns': 'abcdefg'}
|
||||
ret = self.adapter.get_connection_info(10, host, connector)
|
||||
self.assertEqual(10, ret['target_lun'])
|
||||
wwns = ['8899AABBCCDDEEFF', '8899AABBCCDDFFEE']
|
||||
self.assertListEqual(wwns, ret['target_wwn'])
|
||||
|
||||
def test_terminate_connection_auto_zone_enabled(self):
|
||||
connector = {'host': 'host1', 'wwpns': 'abcdefg'}
|
||||
volume = mock.Mock(provider_location='id^lun_41', id='id_41')
|
||||
ret = self.adapter.terminate_connection(volume, connector)
|
||||
self.assertEqual('fibre_channel', ret['driver_volume_type'])
|
||||
data = ret['data']
|
||||
target_map = {
|
||||
'200000051e55a100': ('100000051e55a100', '100000051e55a121'),
|
||||
'200000051e55a121': ('100000051e55a100', '100000051e55a121')}
|
||||
self.assertDictEqual(target_map, data['initiator_target_map'])
|
||||
target_wwn = ['100000051e55a100', '100000051e55a121']
|
||||
self.assertListEqual(target_wwn, data['target_wwn'])
|
||||
|
||||
|
||||
class ISCSIAdapterTest(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.adapter = mock_adapter(adapter.ISCSIAdapter)
|
||||
|
||||
def test_iscsi_protocol(self):
|
||||
stats = self.adapter.update_volume_stats()
|
||||
self.assertEqual('iSCSI', stats['storage_protocol'])
|
||||
|
||||
def test_get_connector_uids(self):
|
||||
connector = {'host': 'fake_host', 'initiator': 'fake_iqn'}
|
||||
ret = self.adapter.get_connector_uids(connector)
|
||||
self.assertListEqual(['fake_iqn'], ret)
|
||||
|
||||
def test_get_connection_info(self):
|
||||
connector = {'host': 'fake_host', 'initiator': 'fake_iqn'}
|
||||
hlu = 10
|
||||
info = self.adapter.get_connection_info(hlu, None, connector)
|
||||
target_iqns = ['iqn.1-1.com.e:c.a.a0', 'iqn.1-1.com.e:c.a.a1']
|
||||
target_portals = ['1.2.3.4:1234', '1.2.3.5:1234']
|
||||
self.assertListEqual(target_iqns, info['target_iqns'])
|
||||
self.assertListEqual([hlu, hlu], info['target_luns'])
|
||||
self.assertListEqual(target_portals, info['target_portals'])
|
||||
self.assertEqual(hlu, info['target_lun'])
|
||||
self.assertTrue(info['target_portal'] in target_portals)
|
||||
self.assertTrue(info['target_iqn'] in target_iqns)
|
433
cinder/tests/unit/volume/drivers/dell_emc/unity/test_client.py
Normal file
433
cinder/tests/unit/volume/drivers/dell_emc/unity/test_client.py
Normal file
@ -0,0 +1,433 @@
|
||||
# Copyright (c) 2016 Dell Inc. or its subsidiaries.
|
||||
# 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 unittest
|
||||
|
||||
from mock import mock
|
||||
from oslo_utils import units
|
||||
|
||||
from cinder.tests.unit.volume.drivers.dell_emc.unity \
|
||||
import fake_exception as ex
|
||||
from cinder.volume.drivers.dell_emc.unity import client
|
||||
|
||||
|
||||
########################
|
||||
#
|
||||
# Start of Mocks
|
||||
#
|
||||
########################
|
||||
|
||||
|
||||
class MockResource(object):
|
||||
def __init__(self, name=None, _id=None):
|
||||
self.name = name
|
||||
self._id = _id
|
||||
self.existed = True
|
||||
self.size_total = 5 * units.Gi
|
||||
self.size_subscribed = 6 * units.Gi
|
||||
self.size_free = 2 * units.Gi
|
||||
self.is_auto_delete = None
|
||||
self.initiator_id = []
|
||||
self.alu_hlu_map = {'already_attached': 99}
|
||||
self.ip_address = None
|
||||
self.is_logged_in = None
|
||||
self.wwn = None
|
||||
self.max_iops = None
|
||||
self.max_kbps = None
|
||||
self.pool_name = 'Pool0'
|
||||
|
||||
def get_id(self):
|
||||
return self._id
|
||||
|
||||
def delete(self):
|
||||
if self.get_id() in ['snap_2']:
|
||||
raise ex.SnapDeleteIsCalled()
|
||||
elif self.get_id() == 'not_found':
|
||||
raise ex.UnityResourceNotFoundError()
|
||||
elif self.get_id() == 'snap_in_use':
|
||||
raise ex.UnityDeleteAttachedSnapError()
|
||||
|
||||
@property
|
||||
def pool(self):
|
||||
return MockResource('pool0')
|
||||
|
||||
@property
|
||||
def iscsi_host_initiators(self):
|
||||
iscsi_initiator = MockResource('iscsi_initiator')
|
||||
iscsi_initiator.initiator_id = ['iqn.1-1.com.e:c.host.0',
|
||||
'iqn.1-1.com.e:c.host.1']
|
||||
return iscsi_initiator
|
||||
|
||||
@property
|
||||
def total_size_gb(self):
|
||||
return self.size_total / units.Gi
|
||||
|
||||
@total_size_gb.setter
|
||||
def total_size_gb(self, value):
|
||||
if value == self.total_size_gb:
|
||||
raise ex.UnityNothingToModifyError()
|
||||
else:
|
||||
self.size_total = value * units.Gi
|
||||
|
||||
def add_initiator(self, uid, force_create=None):
|
||||
self.initiator_id.append(uid)
|
||||
|
||||
def attach(self, lun_or_snap, skip_hlu_0=True):
|
||||
if lun_or_snap.get_id() == 'already_attached':
|
||||
raise ex.UnityResourceAlreadyAttachedError()
|
||||
self.alu_hlu_map[lun_or_snap.get_id()] = len(self.alu_hlu_map)
|
||||
return self.get_hlu(lun_or_snap)
|
||||
|
||||
@staticmethod
|
||||
def detach(lun_or_snap):
|
||||
if lun_or_snap.name == 'detach_failure':
|
||||
raise ex.DetachIsCalled()
|
||||
|
||||
def get_hlu(self, lun):
|
||||
return self.alu_hlu_map.get(lun.get_id(), None)
|
||||
|
||||
@staticmethod
|
||||
def create_lun(lun_name, size_gb, description=None, io_limit_policy=None):
|
||||
if lun_name == 'in_use':
|
||||
raise ex.UnityLunNameInUseError()
|
||||
ret = MockResource(lun_name, 'lun_2')
|
||||
if io_limit_policy is not None:
|
||||
ret.max_iops = io_limit_policy.max_iops
|
||||
ret.max_kbps = io_limit_policy.max_kbps
|
||||
return ret
|
||||
|
||||
@staticmethod
|
||||
def create_snap(name, is_auto_delete=False):
|
||||
if name == 'in_use':
|
||||
raise ex.UnitySnapNameInUseError()
|
||||
ret = MockResource(name)
|
||||
ret.is_auto_delete = is_auto_delete
|
||||
return ret
|
||||
|
||||
@staticmethod
|
||||
def update(data=None):
|
||||
pass
|
||||
|
||||
@property
|
||||
def iscsi_node(self):
|
||||
name = 'iqn.1-1.com.e:c.%s.0' % self.name
|
||||
return MockResource(name)
|
||||
|
||||
@property
|
||||
def fc_host_initiators(self):
|
||||
init0 = MockResource('fhi_0')
|
||||
init0.initiator_id = '00:11:22:33:44:55:66:77:88:99:AA:BB:CC:CD:EE:FF'
|
||||
init1 = MockResource('fhi_1')
|
||||
init1.initiator_id = '00:11:22:33:44:55:66:77:88:99:AA:BB:BC:CD:EE:FF'
|
||||
return MockResourceList.create(init0, init1)
|
||||
|
||||
@property
|
||||
def paths(self):
|
||||
path0 = MockResource('%s_path_0' % self.name)
|
||||
path0.is_logged_in = True
|
||||
path1 = MockResource('%s_path_1' % self.name)
|
||||
path1.is_logged_in = False
|
||||
path2 = MockResource('%s_path_2' % self.name)
|
||||
path2.is_logged_in = True
|
||||
return [path0, path1]
|
||||
|
||||
@property
|
||||
def fc_port(self):
|
||||
ret = MockResource()
|
||||
ret.wwn = '00:11:22:33:44:55:66:77:88:99:AA:BB:CC:DD:EE:FF'
|
||||
return ret
|
||||
|
||||
@property
|
||||
def host_luns(self):
|
||||
return []
|
||||
|
||||
@property
|
||||
def storage_resource(self):
|
||||
return MockResource(_id='sr_%s' % self._id,
|
||||
name='sr_%s' % self.name)
|
||||
|
||||
def modify(self, name=None):
|
||||
self.name = name
|
||||
|
||||
|
||||
class MockResourceList(object):
|
||||
def __init__(self, names):
|
||||
self.resources = [MockResource(name) for name in names]
|
||||
|
||||
@staticmethod
|
||||
def create(*rsc_list):
|
||||
ret = MockResourceList([])
|
||||
ret.resources = rsc_list
|
||||
return ret
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
return map(lambda i: i.name, self.resources)
|
||||
|
||||
def __iter__(self):
|
||||
return self.resources.__iter__()
|
||||
|
||||
def __len__(self):
|
||||
return len(self.resources)
|
||||
|
||||
def __getattr__(self, item):
|
||||
return [getattr(i, item) for i in self.resources]
|
||||
|
||||
|
||||
class MockSystem(object):
|
||||
def __init__(self):
|
||||
self.serial_number = 'SYSTEM_SERIAL'
|
||||
|
||||
@staticmethod
|
||||
def get_lun(_id=None, name=None):
|
||||
if _id == 'not_found':
|
||||
raise ex.UnityResourceNotFoundError()
|
||||
return MockResource(name, _id)
|
||||
|
||||
@staticmethod
|
||||
def get_pool():
|
||||
return MockResourceList(['Pool 1', 'Pool 2'])
|
||||
|
||||
@staticmethod
|
||||
def get_snap(name):
|
||||
if name == 'not_found':
|
||||
raise ex.UnityResourceNotFoundError()
|
||||
return MockResource(name)
|
||||
|
||||
@staticmethod
|
||||
def create_host(name):
|
||||
return MockResource(name)
|
||||
|
||||
@staticmethod
|
||||
def get_host(name):
|
||||
if name == 'not_found':
|
||||
raise ex.UnityResourceNotFoundError()
|
||||
return MockResource(name)
|
||||
|
||||
@staticmethod
|
||||
def get_iscsi_portal():
|
||||
portal0 = MockResource('p0')
|
||||
portal0.ip_address = '1.1.1.1'
|
||||
portal1 = MockResource('p1')
|
||||
portal1.ip_address = '1.1.1.2'
|
||||
return [portal0, portal1]
|
||||
|
||||
@staticmethod
|
||||
def get_fc_port():
|
||||
port0 = MockResource('fcp0')
|
||||
port0.wwn = '00:11:22:33:44:55:66:77:88:99:AA:BB:CC:DD:EE:FF'
|
||||
port1 = MockResource('fcp1')
|
||||
port1.wwn = '00:11:22:33:44:55:66:77:88:99:AA:BB:CC:DD:FF:EE'
|
||||
return [port0, port1]
|
||||
|
||||
@staticmethod
|
||||
def create_io_limit_policy(name, max_iops=None, max_kbps=None):
|
||||
if name == 'in_use':
|
||||
raise ex.UnityPolicyNameInUseError()
|
||||
ret = MockResource(name)
|
||||
ret.max_iops = max_iops
|
||||
ret.max_kbps = max_kbps
|
||||
return ret
|
||||
|
||||
@staticmethod
|
||||
def get_io_limit_policy(name):
|
||||
return MockResource(name=name)
|
||||
|
||||
|
||||
@mock.patch.object(client, 'storops', new='True')
|
||||
def get_client():
|
||||
ret = client.UnityClient('1.2.3.4', 'user', 'pass')
|
||||
ret._system = MockSystem()
|
||||
return ret
|
||||
|
||||
|
||||
########################
|
||||
#
|
||||
# Start of Tests
|
||||
#
|
||||
########################
|
||||
@mock.patch.object(client, 'storops_ex', new=ex)
|
||||
class ClientTest(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.client = get_client()
|
||||
|
||||
def test_get_serial(self):
|
||||
self.assertEqual('SYSTEM_SERIAL', self.client.get_serial())
|
||||
|
||||
def test_create_lun_success(self):
|
||||
name = 'LUN 3'
|
||||
pool = MockResource('Pool 0')
|
||||
lun = self.client.create_lun(name, 5, pool)
|
||||
self.assertEqual(name, lun.name)
|
||||
|
||||
def test_create_lun_name_in_use(self):
|
||||
name = 'in_use'
|
||||
pool = MockResource('Pool 0')
|
||||
lun = self.client.create_lun(name, 6, pool)
|
||||
self.assertEqual('in_use', lun.name)
|
||||
|
||||
def test_create_lun_with_io_limit(self):
|
||||
pool = MockResource('Pool 0')
|
||||
limit = MockResource('limit')
|
||||
limit.max_kbps = 100
|
||||
lun = self.client.create_lun('LUN 4', 6, pool, io_limit_policy=limit)
|
||||
self.assertEqual(100, lun.max_kbps)
|
||||
|
||||
def test_delete_lun_normal(self):
|
||||
self.assertIsNone(self.client.delete_lun('lun3'))
|
||||
|
||||
def test_delete_lun_not_found(self):
|
||||
try:
|
||||
self.client.delete_lun('not_found')
|
||||
except ex.StoropsException:
|
||||
self.fail('not found error should be dealt with silently.')
|
||||
|
||||
def test_get_lun_with_id(self):
|
||||
lun = self.client.get_lun('lun4')
|
||||
self.assertEqual('lun4', lun.get_id())
|
||||
|
||||
def test_get_lun_with_name(self):
|
||||
lun = self.client.get_lun(name='LUN 4')
|
||||
self.assertEqual('LUN 4', lun.name)
|
||||
|
||||
def test_get_lun_not_found(self):
|
||||
ret = self.client.get_lun(lun_id='not_found')
|
||||
self.assertIsNone(ret)
|
||||
|
||||
def test_get_pools(self):
|
||||
pools = self.client.get_pools()
|
||||
self.assertEqual(2, len(pools))
|
||||
|
||||
def test_create_snap_normal(self):
|
||||
snap = self.client.create_snap('lun_1', 'snap_1')
|
||||
self.assertEqual('snap_1', snap.name)
|
||||
|
||||
def test_create_snap_in_use(self):
|
||||
snap = self.client.create_snap('lun_1', 'in_use')
|
||||
self.assertEqual('in_use', snap.name)
|
||||
|
||||
def test_delete_snap_error(self):
|
||||
def f():
|
||||
snap = MockResource(_id='snap_2')
|
||||
self.client.delete_snap(snap)
|
||||
|
||||
self.assertRaises(ex.SnapDeleteIsCalled, f)
|
||||
|
||||
def test_delete_snap_not_found(self):
|
||||
try:
|
||||
snap = MockResource(_id='not_found')
|
||||
self.client.delete_snap(snap)
|
||||
except ex.StoropsException:
|
||||
self.fail('snap not found should not raise exception.')
|
||||
|
||||
def test_delete_snap_none(self):
|
||||
try:
|
||||
ret = self.client.delete_snap(None)
|
||||
self.assertIsNone(ret)
|
||||
except ex.StoropsException:
|
||||
self.fail('delete none should not raise exception.')
|
||||
|
||||
def test_delete_snap_in_use(self):
|
||||
def f():
|
||||
snap = MockResource(_id='snap_in_use')
|
||||
self.client.delete_snap(snap)
|
||||
|
||||
self.assertRaises(ex.UnityDeleteAttachedSnapError, f)
|
||||
|
||||
def test_get_snap_found(self):
|
||||
snap = self.client.get_snap('snap_2')
|
||||
self.assertEqual('snap_2', snap.name)
|
||||
|
||||
def test_get_snap_not_found(self):
|
||||
ret = self.client.get_snap('not_found')
|
||||
self.assertIsNone(ret)
|
||||
|
||||
def test_create_host_found(self):
|
||||
iqns = ['iqn.1-1.com.e:c.a.a0']
|
||||
host = self.client.create_host('host1', iqns)
|
||||
|
||||
self.assertEqual('host1', host.name)
|
||||
self.assertLessEqual(['iqn.1-1.com.e:c.a.a0'], host.initiator_id)
|
||||
|
||||
def test_create_host_not_found(self):
|
||||
host = self.client.create_host('not_found', [])
|
||||
self.assertEqual('not_found', host.name)
|
||||
|
||||
def test_attach_lun(self):
|
||||
lun = MockResource(_id='lun1', name='l1')
|
||||
host = MockResource('host1')
|
||||
self.assertEqual(1, self.client.attach(host, lun))
|
||||
|
||||
def test_attach_already_attached(self):
|
||||
lun = MockResource(_id='already_attached')
|
||||
host = MockResource('host1')
|
||||
hlu = self.client.attach(host, lun)
|
||||
self.assertEqual(99, hlu)
|
||||
|
||||
def test_detach_lun(self):
|
||||
def f():
|
||||
lun = MockResource('detach_failure')
|
||||
host = MockResource('host1')
|
||||
self.client.detach(host, lun)
|
||||
|
||||
self.assertRaises(ex.DetachIsCalled, f)
|
||||
|
||||
def test_get_host(self):
|
||||
self.assertEqual('host2', self.client.get_host('host2').name)
|
||||
|
||||
def test_get_iscsi_target_info(self):
|
||||
ret = self.client.get_iscsi_target_info()
|
||||
expected = [{'iqn': 'iqn.1-1.com.e:c.p0.0', 'portal': '1.1.1.1:3260'},
|
||||
{'iqn': 'iqn.1-1.com.e:c.p1.0', 'portal': '1.1.1.2:3260'}]
|
||||
self.assertListEqual(expected, ret)
|
||||
|
||||
def test_get_fc_target_info_without_host(self):
|
||||
ret = self.client.get_fc_target_info()
|
||||
self.assertListEqual(['8899AABBCCDDEEFF', '8899AABBCCDDFFEE'], ret)
|
||||
|
||||
def test_get_fc_target_info_with_host(self):
|
||||
host = MockResource('host0')
|
||||
ret = self.client.get_fc_target_info(host, True)
|
||||
self.assertListEqual(['8899AABBCCDDEEFF', '8899AABBCCDDEEFF'], ret)
|
||||
|
||||
def test_get_io_limit_policy_none(self):
|
||||
ret = self.client.get_io_limit_policy(None)
|
||||
self.assertIsNone(ret)
|
||||
|
||||
def test_get_io_limit_policy_create_new(self):
|
||||
specs = {'maxBWS': 2, 'id': 'max_2_mbps', 'maxIOPS': None}
|
||||
limit = self.client.get_io_limit_policy(specs)
|
||||
self.assertEqual('max_2_mbps', limit.name)
|
||||
self.assertEqual(2, limit.max_kbps)
|
||||
|
||||
def test_create_io_limit_policy_success(self):
|
||||
limit = self.client.create_io_limit_policy('3kiops', max_iops=3000)
|
||||
self.assertEqual('3kiops', limit.name)
|
||||
self.assertEqual(3000, limit.max_iops)
|
||||
|
||||
def test_create_io_limit_policy_in_use(self):
|
||||
limit = self.client.create_io_limit_policy('in_use', max_iops=100)
|
||||
self.assertEqual('in_use', limit.name)
|
||||
|
||||
def test_expand_lun_success(self):
|
||||
lun = self.client.extend_lun('ev_3', 6)
|
||||
self.assertEqual(6, lun.total_size_gb)
|
||||
|
||||
def test_expand_lun_nothing_to_modify(self):
|
||||
lun = self.client.extend_lun('ev_4', 5)
|
||||
self.assertEqual(5, lun.total_size_gb)
|
||||
|
||||
def test_get_pool_name(self):
|
||||
self.assertEqual('Pool0', self.client.get_pool_name('lun_0'))
|
234
cinder/tests/unit/volume/drivers/dell_emc/unity/test_driver.py
Normal file
234
cinder/tests/unit/volume/drivers/dell_emc/unity/test_driver.py
Normal file
@ -0,0 +1,234 @@
|
||||
# Copyright (c) 2016 Dell Inc. or its subsidiaries.
|
||||
# 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 unittest
|
||||
|
||||
import mock
|
||||
|
||||
from cinder.tests.unit.volume.drivers.dell_emc.unity \
|
||||
import fake_exception as ex
|
||||
from cinder.volume import configuration as conf
|
||||
from cinder.volume.drivers.dell_emc.unity import driver
|
||||
|
||||
|
||||
########################
|
||||
#
|
||||
# Start of Mocks
|
||||
#
|
||||
########################
|
||||
|
||||
class MockAdapter(object):
|
||||
def do_setup(self, driver_object, configuration):
|
||||
raise ex.AdapterSetupError()
|
||||
|
||||
@staticmethod
|
||||
def create_volume(volume):
|
||||
return volume
|
||||
|
||||
@staticmethod
|
||||
def create_volume_from_snapshot(volume, snapshot):
|
||||
return volume
|
||||
|
||||
@staticmethod
|
||||
def create_cloned_volume(volume, src_vref):
|
||||
return volume
|
||||
|
||||
@staticmethod
|
||||
def extend_volume(volume, new_size):
|
||||
volume.size = new_size
|
||||
|
||||
@staticmethod
|
||||
def delete_volume(volume):
|
||||
volume.exists = False
|
||||
|
||||
@staticmethod
|
||||
def create_snapshot(snapshot):
|
||||
snapshot.exists = True
|
||||
|
||||
@staticmethod
|
||||
def delete_snapshot(snapshot):
|
||||
snapshot.exists = False
|
||||
|
||||
@staticmethod
|
||||
def initialize_connection(volume, connector):
|
||||
return {'volume': volume, 'connector': connector}
|
||||
|
||||
@staticmethod
|
||||
def terminate_connection(volume, connector):
|
||||
return {'volume': volume, 'connector': connector}
|
||||
|
||||
@staticmethod
|
||||
def update_volume_stats():
|
||||
return {'stats': 123}
|
||||
|
||||
@staticmethod
|
||||
def manage_existing(volume, existing_ref):
|
||||
volume.managed = True
|
||||
return volume
|
||||
|
||||
@staticmethod
|
||||
def manage_existing_get_size(volume, existing_ref):
|
||||
volume.managed = True
|
||||
volume.size = 7
|
||||
return volume
|
||||
|
||||
@staticmethod
|
||||
def get_pool_name(volume):
|
||||
return 'pool_0'
|
||||
|
||||
|
||||
########################
|
||||
#
|
||||
# Start of Tests
|
||||
#
|
||||
########################
|
||||
|
||||
class UnityDriverTest(unittest.TestCase):
|
||||
@staticmethod
|
||||
def get_volume():
|
||||
return mock.Mock(provider_location='id^lun_43', id='id_43')
|
||||
|
||||
@classmethod
|
||||
def get_snapshot(cls):
|
||||
return mock.Mock(volume=cls.get_volume())
|
||||
|
||||
@staticmethod
|
||||
def get_context():
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def get_connector():
|
||||
return {'host': 'host1'}
|
||||
|
||||
def setUp(self):
|
||||
self.config = conf.Configuration(None)
|
||||
self.driver = driver.UnityDriver(configuration=self.config)
|
||||
self.driver.adapter = MockAdapter()
|
||||
|
||||
def test_default_initialize(self):
|
||||
config = conf.Configuration(None)
|
||||
iscsi_driver = driver.UnityDriver(configuration=config)
|
||||
self.assertIsNone(config.unity_storage_pool_names)
|
||||
self.assertTrue(config.san_thin_provision)
|
||||
self.assertEqual('', config.san_ip)
|
||||
self.assertEqual('admin', config.san_login)
|
||||
self.assertEqual('', config.san_password)
|
||||
self.assertEqual('', config.san_private_key)
|
||||
self.assertEqual('', config.san_clustername)
|
||||
self.assertEqual(22, config.san_ssh_port)
|
||||
self.assertEqual(False, config.san_is_local)
|
||||
self.assertEqual(30, config.ssh_conn_timeout)
|
||||
self.assertEqual(1, config.ssh_min_pool_conn)
|
||||
self.assertEqual(5, config.ssh_max_pool_conn)
|
||||
self.assertEqual('iSCSI', iscsi_driver.protocol)
|
||||
|
||||
def test_fc_initialize(self):
|
||||
config = conf.Configuration(None)
|
||||
config.storage_protocol = 'fc'
|
||||
fc_driver = driver.UnityDriver(configuration=config)
|
||||
self.assertEqual('FC', fc_driver.protocol)
|
||||
|
||||
def test_do_setup(self):
|
||||
def f():
|
||||
self.driver.do_setup(None)
|
||||
|
||||
self.assertRaises(ex.AdapterSetupError, f)
|
||||
|
||||
def test_create_volume(self):
|
||||
volume = self.get_volume()
|
||||
self.assertEqual(volume, self.driver.create_volume(volume))
|
||||
|
||||
def test_create_volume_from_snapshot(self):
|
||||
volume = self.get_volume()
|
||||
snap = self.get_snapshot()
|
||||
self.assertEqual(
|
||||
volume, self.driver.create_volume_from_snapshot(volume, snap))
|
||||
|
||||
def test_create_cloned_volume(self):
|
||||
volume = self.get_volume()
|
||||
self.assertEqual(
|
||||
volume, self.driver.create_cloned_volume(volume, None))
|
||||
|
||||
def test_extend_volume(self):
|
||||
volume = self.get_volume()
|
||||
self.driver.extend_volume(volume, 6)
|
||||
self.assertEqual(6, volume.size)
|
||||
|
||||
def test_delete_volume(self):
|
||||
volume = self.get_volume()
|
||||
self.driver.delete_volume(volume)
|
||||
self.assertFalse(volume.exists)
|
||||
|
||||
def test_create_snapshot(self):
|
||||
snapshot = self.get_snapshot()
|
||||
self.driver.create_snapshot(snapshot)
|
||||
self.assertTrue(snapshot.exists)
|
||||
|
||||
def test_delete_snapshot(self):
|
||||
snapshot = self.get_snapshot()
|
||||
self.driver.delete_snapshot(snapshot)
|
||||
self.assertFalse(snapshot.exists)
|
||||
|
||||
def test_ensure_export(self):
|
||||
self.assertIsNone(self.driver.ensure_export(
|
||||
self.get_context(), self.get_volume()))
|
||||
|
||||
def test_create_export(self):
|
||||
self.assertIsNone(self.driver.create_export(
|
||||
self.get_context(), self.get_volume(), self.get_connector()))
|
||||
|
||||
def test_remove_export(self):
|
||||
self.assertIsNone(self.driver.remove_export(
|
||||
self.get_context(), self.get_volume()))
|
||||
|
||||
def test_check_for_export(self):
|
||||
self.assertIsNone(self.driver.check_for_export(
|
||||
self.get_context(), self.get_volume()))
|
||||
|
||||
def test_initialize_connection(self):
|
||||
volume = self.get_volume()
|
||||
connector = self.get_connector()
|
||||
conn_info = self.driver.initialize_connection(volume, connector)
|
||||
self.assertEqual(volume, conn_info['volume'])
|
||||
self.assertEqual(connector, conn_info['connector'])
|
||||
|
||||
def test_terminate_connection(self):
|
||||
volume = self.get_volume()
|
||||
connector = self.get_connector()
|
||||
conn_info = self.driver.terminate_connection(volume, connector)
|
||||
self.assertEqual(volume, conn_info['volume'])
|
||||
self.assertEqual(connector, conn_info['connector'])
|
||||
|
||||
def test_update_volume_stats(self):
|
||||
stats = self.driver.get_volume_stats(True)
|
||||
self.assertEqual(123, stats['stats'])
|
||||
self.assertEqual(self.driver.VERSION, stats['driver_version'])
|
||||
self.assertEqual(self.driver.VENDOR, stats['vendor_name'])
|
||||
|
||||
def test_manage_existing(self):
|
||||
volume = self.driver.manage_existing(self.get_volume(), None)
|
||||
self.assertTrue(volume.managed)
|
||||
|
||||
def test_manage_existing_get_size(self):
|
||||
volume = self.driver.manage_existing_get_size(self.get_volume(), None)
|
||||
self.assertTrue(volume.managed)
|
||||
self.assertEqual(7, volume.size)
|
||||
|
||||
def test_get_pool(self):
|
||||
self.assertEqual('pool_0', self.driver.get_pool(self.get_volume()))
|
||||
|
||||
def test_unmanage(self):
|
||||
ret = self.driver.unmanage(None)
|
||||
self.assertIsNone(ret)
|
254
cinder/tests/unit/volume/drivers/dell_emc/unity/test_utils.py
Normal file
254
cinder/tests/unit/volume/drivers/dell_emc/unity/test_utils.py
Normal file
@ -0,0 +1,254 @@
|
||||
# Copyright (c) 2016 Dell Inc. or its subsidiaries.
|
||||
# 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 functools
|
||||
import unittest
|
||||
|
||||
import mock
|
||||
from oslo_utils import units
|
||||
|
||||
from cinder import exception
|
||||
from cinder.volume.drivers.dell_emc.unity import utils
|
||||
|
||||
|
||||
def get_volume_type_extra_specs(volume_type):
|
||||
return {'provisioning:type': volume_type}
|
||||
|
||||
|
||||
def get_volume_type_qos_specs(type_id):
|
||||
if type_id == 'invalid_backend_qos_consumer':
|
||||
ret = {'qos_specs': {'consumer': 'invalid'}}
|
||||
elif type_id == 'both_none':
|
||||
ret = {'qos_specs': {'consumer': 'back-end', 'specs': {}}}
|
||||
elif type_id == 'max_1000_iops':
|
||||
ret = {
|
||||
'qos_specs': {
|
||||
'id': 'max_1000_iops',
|
||||
'consumer': 'both',
|
||||
'specs': {
|
||||
'maxIOPS': 1000
|
||||
}
|
||||
}
|
||||
}
|
||||
elif type_id == 'max_2_mbps':
|
||||
ret = {
|
||||
'qos_specs': {
|
||||
'id': 'max_2_mbps',
|
||||
'consumer': 'back-end',
|
||||
'specs': {
|
||||
'maxBWS': 2
|
||||
}
|
||||
}
|
||||
}
|
||||
else:
|
||||
ret = None
|
||||
return ret
|
||||
|
||||
|
||||
def patch_volume_types(func):
|
||||
@functools.wraps(func)
|
||||
@mock.patch(target=('cinder.volume.volume_types'
|
||||
'.get_volume_type_extra_specs'),
|
||||
new=get_volume_type_extra_specs)
|
||||
@mock.patch(target=('cinder.volume.volume_types'
|
||||
'.get_volume_type_qos_specs'),
|
||||
new=get_volume_type_qos_specs)
|
||||
def func_wrapper(*args, **kwargs):
|
||||
return func(*args, **kwargs)
|
||||
|
||||
return func_wrapper
|
||||
|
||||
|
||||
class UnityUtilsTest(unittest.TestCase):
|
||||
def test_validate_pool_names_filter(self):
|
||||
all_pools = list('acd')
|
||||
pool_names = utils.validate_pool_names(list('abc'), all_pools)
|
||||
self.assertIn('a', pool_names)
|
||||
self.assertIn('c', pool_names)
|
||||
self.assertNotIn('b', pool_names)
|
||||
self.assertNotIn('d', pool_names)
|
||||
|
||||
def test_validate_pool_names_non_exists(self):
|
||||
def f():
|
||||
all_pools = list('abc')
|
||||
utils.validate_pool_names(list('efg'), all_pools)
|
||||
|
||||
self.assertRaises(exception.VolumeBackendAPIException, f)
|
||||
|
||||
def test_validate_pool_names_default(self):
|
||||
all_pools = list('ab')
|
||||
pool_names = utils.validate_pool_names([], all_pools)
|
||||
self.assertEqual(2, len(pool_names))
|
||||
|
||||
pool_names = utils.validate_pool_names(None, all_pools)
|
||||
self.assertEqual(2, len(pool_names))
|
||||
|
||||
def test_build_provider_location(self):
|
||||
location = utils.build_provider_location('unity', 'thin', 'ev_1', '3')
|
||||
expected = 'id^ev_1|system^unity|type^thin|version^3'
|
||||
self.assertEqual(expected, location)
|
||||
|
||||
def test_extract_provider_location_version(self):
|
||||
location = 'id^ev_1|system^unity|type^thin|version^3'
|
||||
self.assertEqual('3',
|
||||
utils.extract_provider_location(location, 'version'))
|
||||
|
||||
def test_extract_provider_location_type(self):
|
||||
location = 'id^ev_1|system^unity|type^thin|version^3'
|
||||
self.assertEqual('thin',
|
||||
utils.extract_provider_location(location, 'type'))
|
||||
|
||||
def test_extract_provider_location_system(self):
|
||||
location = 'id^ev_1|system^unity|type^thin|version^3'
|
||||
self.assertEqual('unity',
|
||||
utils.extract_provider_location(location, 'system'))
|
||||
|
||||
def test_extract_provider_location_id(self):
|
||||
location = 'id^ev_1|system^unity|type^thin|version^3'
|
||||
self.assertEqual('ev_1',
|
||||
utils.extract_provider_location(location, 'id'))
|
||||
|
||||
def test_extract_provider_location_not_found(self):
|
||||
location = 'id^ev_1|system^unity|type^thin|version^3'
|
||||
self.assertIsNone(utils.extract_provider_location(location, 'na'))
|
||||
|
||||
def test_extract_provider_location_none(self):
|
||||
self.assertIsNone(utils.extract_provider_location(None, 'abc'))
|
||||
|
||||
def test_extract_iscsi_uids(self):
|
||||
connector = {'host': 'fake_host',
|
||||
'initiator': 'fake_iqn'}
|
||||
self.assertEqual(['fake_iqn'],
|
||||
utils.extract_iscsi_uids(connector))
|
||||
|
||||
def test_extract_iscsi_uids_not_found(self):
|
||||
connector = {'host': 'fake_host'}
|
||||
self.assertRaises(exception.VolumeBackendAPIException,
|
||||
utils.extract_iscsi_uids,
|
||||
connector)
|
||||
|
||||
def test_extract_fc_uids(self):
|
||||
connector = {'host': 'fake_host',
|
||||
'wwnns': ['1111111111111111',
|
||||
'2222222222222222'],
|
||||
'wwpns': ['3333333333333333',
|
||||
'4444444444444444']
|
||||
}
|
||||
self.assertEqual(['11:11:11:11:11:11:11:11:33:33:33:33:33:33:33:33',
|
||||
'22:22:22:22:22:22:22:22:44:44:44:44:44:44:44:44', ],
|
||||
utils.extract_fc_uids(connector))
|
||||
|
||||
def test_extract_fc_uids_not_found(self):
|
||||
connector = {'host': 'fake_host'}
|
||||
self.assertRaises(exception.VolumeBackendAPIException,
|
||||
utils.extract_iscsi_uids,
|
||||
connector)
|
||||
|
||||
def test_byte_to_gib(self):
|
||||
self.assertEqual(5, utils.byte_to_gib(5 * units.Gi))
|
||||
|
||||
def test_byte_to_mib(self):
|
||||
self.assertEqual(5, utils.byte_to_mib(5 * units.Mi))
|
||||
|
||||
def test_gib_to_mib(self):
|
||||
self.assertEqual(5 * units.Gi / units.Mi, utils.gib_to_mib(5))
|
||||
|
||||
def test_convert_ip_to_portal(self):
|
||||
self.assertEqual('1.2.3.4:3260', utils.convert_ip_to_portal('1.2.3.4'))
|
||||
|
||||
def test_convert_to_itor_tgt_map(self):
|
||||
zone_mapping = {
|
||||
'san_1': {
|
||||
'initiator_port_wwn_list':
|
||||
('200000051e55a100', '200000051e55a121'),
|
||||
'target_port_wwn_list':
|
||||
('100000051e55a100', '100000051e55a121')
|
||||
}
|
||||
}
|
||||
ret = utils.convert_to_itor_tgt_map(zone_mapping)
|
||||
self.assertEqual(['100000051e55a100', '100000051e55a121'], ret[0])
|
||||
mapping = ret[1]
|
||||
targets = ('100000051e55a100', '100000051e55a121')
|
||||
self.assertEqual(targets, mapping['200000051e55a100'])
|
||||
self.assertEqual(targets, mapping['200000051e55a121'])
|
||||
|
||||
def test_get_pool_name(self):
|
||||
volume = mock.Mock(host='host@backend#pool_name')
|
||||
self.assertEqual('pool_name', utils.get_pool_name(volume))
|
||||
|
||||
def test_ignore_exception(self):
|
||||
class IgnoredException(Exception):
|
||||
pass
|
||||
|
||||
def f():
|
||||
raise IgnoredException('any exception')
|
||||
|
||||
try:
|
||||
utils.ignore_exception(f)
|
||||
except IgnoredException:
|
||||
self.fail('should not raise any exception.')
|
||||
|
||||
def test_assure_cleanup(self):
|
||||
data = [0]
|
||||
|
||||
def _enter():
|
||||
data[0] += 10
|
||||
return data[0]
|
||||
|
||||
def _exit(x):
|
||||
data[0] = x - 1
|
||||
|
||||
ctx = utils.assure_cleanup(_enter, _exit, True)
|
||||
with ctx as r:
|
||||
self.assertEqual(10, r)
|
||||
|
||||
self.assertEqual(9, data[0])
|
||||
|
||||
def test_get_backend_qos_specs_type_none(self):
|
||||
volume = mock.Mock(volume_type_id=None)
|
||||
ret = utils.get_backend_qos_specs(volume)
|
||||
self.assertIsNone(ret)
|
||||
|
||||
@patch_volume_types
|
||||
def test_get_backend_qos_specs_none(self):
|
||||
volume = mock.Mock(volume_type_id='no_qos')
|
||||
ret = utils.get_backend_qos_specs(volume)
|
||||
self.assertIsNone(ret)
|
||||
|
||||
@patch_volume_types
|
||||
def test_get_backend_qos_invalid_consumer(self):
|
||||
volume = mock.Mock(volume_type_id='invalid_backend_qos_consumer')
|
||||
ret = utils.get_backend_qos_specs(volume)
|
||||
self.assertIsNone(ret)
|
||||
|
||||
@patch_volume_types
|
||||
def test_get_backend_qos_both_none(self):
|
||||
volume = mock.Mock(volume_type_id='both_none')
|
||||
ret = utils.get_backend_qos_specs(volume)
|
||||
self.assertIsNone(ret)
|
||||
|
||||
@patch_volume_types
|
||||
def test_get_backend_qos_iops(self):
|
||||
volume = mock.Mock(volume_type_id='max_1000_iops')
|
||||
ret = utils.get_backend_qos_specs(volume)
|
||||
expected = {'maxBWS': None, 'id': 'max_1000_iops', 'maxIOPS': 1000}
|
||||
self.assertEqual(expected, ret)
|
||||
|
||||
@patch_volume_types
|
||||
def test_get_backend_qos_mbps(self):
|
||||
volume = mock.Mock(volume_type_id='max_2_mbps')
|
||||
ret = utils.get_backend_qos_specs(volume)
|
||||
expected = {'maxBWS': 2, 'id': 'max_2_mbps', 'maxIOPS': None}
|
||||
self.assertEqual(expected, ret)
|
18
cinder/volume/drivers/dell_emc/unity/__init__.py
Normal file
18
cinder/volume/drivers/dell_emc/unity/__init__.py
Normal file
@ -0,0 +1,18 @@
|
||||
# Copyright (c) 2016 Dell Inc. or its subsidiaries.
|
||||
# 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.
|
||||
|
||||
from cinder.volume.drivers.dell_emc.unity import driver
|
||||
|
||||
Driver = driver.UnityDriver
|
523
cinder/volume/drivers/dell_emc/unity/adapter.py
Normal file
523
cinder/volume/drivers/dell_emc/unity/adapter.py
Normal file
@ -0,0 +1,523 @@
|
||||
# Copyright (c) 2016 Dell Inc. or its subsidiaries.
|
||||
# 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 contextlib
|
||||
import functools
|
||||
import random
|
||||
|
||||
from oslo_log import log as logging
|
||||
from oslo_utils import excutils
|
||||
|
||||
from cinder import exception
|
||||
from cinder import utils as cinder_utils
|
||||
from cinder.i18n import _, _LE, _LI
|
||||
from cinder.volume.drivers.dell_emc.unity import client
|
||||
from cinder.volume.drivers.dell_emc.unity import utils
|
||||
from cinder.volume import utils as vol_utils
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
PROTOCOL_FC = 'FC'
|
||||
PROTOCOL_ISCSI = 'iSCSI'
|
||||
|
||||
|
||||
class CommonAdapter(object):
|
||||
protocol = 'unknown'
|
||||
driver_name = 'UnityAbstractDriver'
|
||||
driver_volume_type = 'unknown'
|
||||
|
||||
def __init__(self, version=None):
|
||||
self.version = version
|
||||
self.driver = None
|
||||
self.configured_pool_names = None
|
||||
self.reserved_percentage = None
|
||||
self.max_over_subscription_ratio = None
|
||||
self.volume_backend_name = None
|
||||
self.ip = None
|
||||
self.username = None
|
||||
self.password = None
|
||||
self.array_cert_verify = None
|
||||
self.array_ca_cert_path = None
|
||||
|
||||
self._serial_number = None
|
||||
self.storage_pools_map = None
|
||||
self._client = None
|
||||
|
||||
def do_setup(self, driver, conf):
|
||||
self.driver = driver
|
||||
self.configured_pool_names = conf.unity_storage_pool_names
|
||||
self.reserved_percentage = conf.reserved_percentage
|
||||
self.max_over_subscription_ratio = conf.max_over_subscription_ratio
|
||||
self.volume_backend_name = (conf.safe_get('volume_backend_name') or
|
||||
self.driver_name)
|
||||
self.ip = conf.san_ip
|
||||
self.username = conf.san_login
|
||||
self.password = conf.san_password
|
||||
# Unity currently not support to upload certificate.
|
||||
# Once it supports, enable the verify.
|
||||
self.array_cert_verify = False
|
||||
self.array_ca_cert_path = conf.driver_ssl_cert_path
|
||||
|
||||
self.storage_pools_map = self.get_managed_pools()
|
||||
|
||||
@property
|
||||
def verify_cert(self):
|
||||
verify_cert = self.array_cert_verify
|
||||
if verify_cert and self.array_ca_cert_path is not None:
|
||||
verify_cert = self.array_ca_cert_path
|
||||
return verify_cert
|
||||
|
||||
@property
|
||||
def client(self):
|
||||
if self._client is None:
|
||||
self._client = client.UnityClient(
|
||||
self.ip,
|
||||
self.username,
|
||||
self.password,
|
||||
verify_cert=self.verify_cert)
|
||||
return self._client
|
||||
|
||||
@property
|
||||
def serial_number(self):
|
||||
if self._serial_number is None:
|
||||
self._serial_number = self.client.get_serial()
|
||||
return self._serial_number
|
||||
|
||||
def get_managed_pools(self):
|
||||
names = self.configured_pool_names
|
||||
array_pools = self.client.get_pools()
|
||||
valid_names = utils.validate_pool_names(names, array_pools.name)
|
||||
return {p.name: p for p in array_pools if p.name in valid_names}
|
||||
|
||||
def create_volume(self, volume):
|
||||
"""Creates a volume.
|
||||
|
||||
:param volume: volume information
|
||||
"""
|
||||
volume_size = volume.size
|
||||
volume_name = volume.name
|
||||
volume_description = (volume.display_description
|
||||
if volume.display_description
|
||||
else volume.display_name)
|
||||
|
||||
pool = self._get_target_pool(volume)
|
||||
qos_specs = utils.get_backend_qos_specs(volume)
|
||||
limit_policy = self.client.get_io_limit_policy(qos_specs)
|
||||
|
||||
LOG.info(_LI('Create Volume: %(volume)s Size: %(size)s '
|
||||
'Pool: %(pool)s Qos: %(qos)s.'),
|
||||
{'volume': volume_name,
|
||||
'size': volume_size,
|
||||
'pool': pool.name,
|
||||
'qos': qos_specs})
|
||||
|
||||
lun = self.client.create_lun(
|
||||
volume_name, volume_size, pool, description=volume_description,
|
||||
io_limit_policy=limit_policy)
|
||||
location = self._build_provider_location(
|
||||
lun_type='lun',
|
||||
lun_id=lun.get_id())
|
||||
model_update = {'provider_location': location}
|
||||
return model_update
|
||||
|
||||
def delete_volume(self, volume):
|
||||
lun_id = self.get_lun_id(volume)
|
||||
if lun_id is None:
|
||||
LOG.info(_LI('Backend LUN not found, skipping the deletion. '
|
||||
'Volume: %(volume_name)s.'),
|
||||
{'volume_name': volume.name})
|
||||
else:
|
||||
self.client.delete_lun(lun_id)
|
||||
|
||||
def _initialize_connection(self, lun_or_snap, connector, vol_id):
|
||||
host = self.client.create_host(connector['host'],
|
||||
self.get_connector_uids(connector))
|
||||
hlu = self.client.attach(host, lun_or_snap)
|
||||
data = self.get_connection_info(hlu, host, connector)
|
||||
data['target_discovered'] = True
|
||||
if vol_id is not None:
|
||||
data['volume_id'] = vol_id
|
||||
conn_info = {
|
||||
'driver_volume_type': self.driver_volume_type,
|
||||
'data': data,
|
||||
}
|
||||
LOG.debug('Initialized connection info: %s', conn_info)
|
||||
return conn_info
|
||||
|
||||
def initialize_connection(self, volume, connector):
|
||||
lun = self.client.get_lun(lun_id=self.get_lun_id(volume))
|
||||
return self._initialize_connection(lun, connector, volume.id)
|
||||
|
||||
def _terminate_connection(self, lun_or_snap, connector):
|
||||
host = self.client.get_host(connector['host'])
|
||||
self.client.detach(host, lun_or_snap)
|
||||
|
||||
def terminate_connection(self, volume, connector):
|
||||
lun = self.client.get_lun(lun_id=self.get_lun_id(volume))
|
||||
self._terminate_connection(lun, connector)
|
||||
|
||||
def get_connector_uids(self, connector):
|
||||
return None
|
||||
|
||||
def get_connection_info(self, hlu, host, connector):
|
||||
return {}
|
||||
|
||||
def extend_volume(self, volume, new_size):
|
||||
lun_id = self.get_lun_id(volume)
|
||||
if lun_id is None:
|
||||
msg = (_('Backend LUN not found for Volume: %(volume_name)s.') %
|
||||
{'volume_name': volume.name})
|
||||
raise exception.VolumeBackendAPIException(data=msg)
|
||||
else:
|
||||
self.client.extend_lun(lun_id, new_size)
|
||||
|
||||
def _get_target_pool(self, volume):
|
||||
return self.storage_pools_map[utils.get_pool_name(volume)]
|
||||
|
||||
def _build_provider_location(self, lun_id=None, lun_type=None):
|
||||
return utils.build_provider_location(
|
||||
system=self.serial_number,
|
||||
lun_type=lun_type,
|
||||
lun_id=lun_id,
|
||||
version=self.version)
|
||||
|
||||
def update_volume_stats(self):
|
||||
return {
|
||||
'volume_backend_name': self.volume_backend_name,
|
||||
'storage_protocol': self.protocol,
|
||||
'thin_provisioning_support': True,
|
||||
'thick_provisioning_support': False,
|
||||
'pools': self.get_pools_stats(),
|
||||
}
|
||||
|
||||
def get_pools_stats(self):
|
||||
self.storage_pools_map = self.get_managed_pools()
|
||||
return [self._get_pool_stats(pool) for pool in self.pools]
|
||||
|
||||
@property
|
||||
def pools(self):
|
||||
return self.storage_pools_map.values()
|
||||
|
||||
def _get_pool_stats(self, pool):
|
||||
return {
|
||||
'pool_name': pool.name,
|
||||
'total_capacity_gb': utils.byte_to_gib(pool.size_total),
|
||||
'provisioned_capacity_gb': utils.byte_to_gib(
|
||||
pool.size_subscribed),
|
||||
'free_capacity_gb': utils.byte_to_gib(pool.size_free),
|
||||
'reserved_percentage': self.reserved_percentage,
|
||||
'location_info': ('%(pool_name)s|%(array_serial)s' %
|
||||
{'pool_name': pool.name,
|
||||
'array_serial': self.serial_number}),
|
||||
'thin_provisioning_support': True,
|
||||
'thick_provisioning_support': False,
|
||||
'max_over_subscription_ratio': (
|
||||
self.max_over_subscription_ratio)}
|
||||
|
||||
def get_lun_id(self, volume):
|
||||
"""Retrieves id of the volume's backing LUN.
|
||||
|
||||
:param volume: volume information
|
||||
"""
|
||||
if volume.provider_location:
|
||||
return utils.extract_provider_location(volume.provider_location,
|
||||
'id')
|
||||
else:
|
||||
# In some cases, cinder will not update volume info in DB with
|
||||
# provider_location returned by us. We need to retrieve the id
|
||||
# from array.
|
||||
lun = self.client.get_lun(name=volume.name)
|
||||
return lun.get_id() if lun is not None else None
|
||||
|
||||
def create_snapshot(self, snapshot):
|
||||
"""Creates a snapshot.
|
||||
|
||||
:param snapshot: snapshot information.
|
||||
"""
|
||||
src_lun_id = self.get_lun_id(snapshot.volume)
|
||||
return self.client.create_snap(src_lun_id, snapshot.name)
|
||||
|
||||
def delete_snapshot(self, snapshot):
|
||||
"""Deletes a snapshot.
|
||||
|
||||
:param snapshot: the snapshot to delete.
|
||||
"""
|
||||
snap = self.client.get_snap(name=snapshot.name)
|
||||
self.client.delete_snap(snap)
|
||||
|
||||
def _get_referenced_lun(self, existing_ref):
|
||||
if 'source-id' in existing_ref:
|
||||
lun = self.client.get_lun(lun_id=existing_ref['source-id'])
|
||||
elif 'source-name' in existing_ref:
|
||||
lun = self.client.get_lun(name=existing_ref['source-name'])
|
||||
else:
|
||||
reason = _('Reference must contain source-id or source-name key.')
|
||||
raise exception.ManageExistingInvalidReference(
|
||||
existing_ref=existing_ref, reason=reason)
|
||||
if lun is None or not lun.existed:
|
||||
raise exception.ManageExistingInvalidReference(
|
||||
existing_ref=existing_ref,
|
||||
reason=_("LUN doesn't exist."))
|
||||
return lun
|
||||
|
||||
def manage_existing(self, volume, existing_ref):
|
||||
"""Manages an existing LUN in the array.
|
||||
|
||||
The LUN should be in a manageable pool backend, otherwise return error.
|
||||
Rename the backend storage object so that it matches the
|
||||
`volume['name']` which is how drivers traditionally map between a
|
||||
cinder volume and the associated backend storage object.
|
||||
|
||||
LUN ID or name are supported in `existing_ref`, like:
|
||||
|
||||
.. code-block::
|
||||
|
||||
existing_ref:{
|
||||
'source-id':<LUN id in Unity>
|
||||
}
|
||||
|
||||
or
|
||||
|
||||
.. code-block::
|
||||
|
||||
existing_ref:{
|
||||
'source-name':<LUN name in Unity>
|
||||
}
|
||||
"""
|
||||
lun = self._get_referenced_lun(existing_ref)
|
||||
lun.modify(name=volume.name)
|
||||
return {'provider_location':
|
||||
self._build_provider_location(lun_id=lun.get_id(),
|
||||
lun_type='lun')}
|
||||
|
||||
def manage_existing_get_size(self, volume, existing_ref):
|
||||
"""Returns size of volume to be managed by `manage_existing`.
|
||||
|
||||
The driver does some check here:
|
||||
1. The LUN `existing_ref` should be managed by the `volume.host`.
|
||||
"""
|
||||
lun = self._get_referenced_lun(existing_ref)
|
||||
target_pool_name = utils.get_pool_name(volume)
|
||||
lun_pool_name = lun.pool.name
|
||||
if target_pool_name and lun_pool_name != target_pool_name:
|
||||
reason = (_('The imported LUN is in pool %(pool_name)s '
|
||||
'which is not managed by the host %(host)s.') %
|
||||
{'pool_name': lun_pool_name,
|
||||
'host': volume.host})
|
||||
raise exception.ManageExistingInvalidReference(
|
||||
existing_ref=existing_ref, reason=reason)
|
||||
|
||||
return utils.byte_to_gib(lun.size_total)
|
||||
|
||||
def _disconnect_device(self, conn):
|
||||
conn['connector'].disconnect_volume(conn['conn']['data'],
|
||||
conn['device'])
|
||||
|
||||
def _connect_device(self, conn):
|
||||
return self.driver._connect_device(conn)
|
||||
|
||||
@contextlib.contextmanager
|
||||
def _connect_resource(self, lun_or_snap, connector, res_id):
|
||||
"""Connects to LUN or snapshot, and makes sure disconnect finally.
|
||||
|
||||
:param lun_or_snap: the LUN or snapshot to connect/disconnect.
|
||||
:param connector: the host connector information.
|
||||
:param res_id: the ID of the LUN or snapshot.
|
||||
|
||||
:return the connection information, in a dict with format like (same as
|
||||
the one returned by `_connect_device`):
|
||||
{
|
||||
'conn': <info returned by `initialize_connection`>,
|
||||
'device': <value returned by `connect_volume`>,
|
||||
'connector': <host connector info>
|
||||
}
|
||||
"""
|
||||
init_conn_func = functools.partial(self._initialize_connection,
|
||||
lun_or_snap, connector, res_id)
|
||||
term_conn_func = functools.partial(self._terminate_connection,
|
||||
lun_or_snap, connector)
|
||||
with utils.assure_cleanup(init_conn_func, term_conn_func,
|
||||
False) as conn_info:
|
||||
conn_device_func = functools.partial(self._connect_device,
|
||||
conn_info)
|
||||
with utils.assure_cleanup(conn_device_func,
|
||||
self._disconnect_device,
|
||||
True) as attach_info:
|
||||
yield attach_info
|
||||
|
||||
def _create_volume_from_snap(self, volume, snap, size_in_m=None):
|
||||
"""Creates a volume from a Unity snapshot.
|
||||
|
||||
It attaches the `volume` and `snap`, then use `dd` to copy the
|
||||
data from the Unity snapshot to the `volume`.
|
||||
"""
|
||||
model_update = self.create_volume(volume)
|
||||
volume.provider_location = model_update['provider_location']
|
||||
src_id = snap.get_id()
|
||||
dest_lun = self.client.get_lun(lun_id=self.get_lun_id(volume))
|
||||
try:
|
||||
conn_props = cinder_utils.brick_get_connector_properties()
|
||||
|
||||
with self._connect_resource(dest_lun, conn_props,
|
||||
volume.id) as dest_info, \
|
||||
self._connect_resource(snap, conn_props,
|
||||
src_id) as src_info:
|
||||
if size_in_m is None:
|
||||
# If size is not specified, need to get the size from LUN
|
||||
# of snapshot.
|
||||
lun = self.client.get_lun(
|
||||
lun_id=snap.storage_resource.get_id())
|
||||
size_in_m = utils.byte_to_mib(lun.size_total)
|
||||
vol_utils.copy_volume(
|
||||
src_info['device']['path'],
|
||||
dest_info['device']['path'],
|
||||
size_in_m,
|
||||
self.driver.configuration.volume_dd_blocksize,
|
||||
sparse=True)
|
||||
except Exception:
|
||||
with excutils.save_and_reraise_exception():
|
||||
utils.ignore_exception(self.delete_volume, volume)
|
||||
LOG.error(_LE('Failed to create cloned volume: %(vol_id)s, '
|
||||
'from source unity snapshot: %(snap_name)s. '),
|
||||
{'vol_id': volume.id, 'snap_name': snap.name})
|
||||
|
||||
return model_update
|
||||
|
||||
def create_volume_from_snapshot(self, volume, snapshot):
|
||||
snap = self.client.get_snap(snapshot.name)
|
||||
return self._create_volume_from_snap(volume, snap)
|
||||
|
||||
def create_cloned_volume(self, volume, src_vref):
|
||||
"""Creates cloned volume.
|
||||
|
||||
1. Take an internal snapshot of source volume, and attach it.
|
||||
2. Create a new volume, and attach it.
|
||||
3. Copy from attached snapshot of step 1 to the volume of step 2.
|
||||
4. Delete the internal snapshot created in step 1.
|
||||
"""
|
||||
|
||||
src_lun_id = self.get_lun_id(src_vref)
|
||||
if src_lun_id is None:
|
||||
raise exception.VolumeBackendAPIException(
|
||||
data=_("LUN ID of source volume: %s not found.") %
|
||||
src_vref.name)
|
||||
src_snap_name = 'snap_clone_%s' % volume.id
|
||||
|
||||
create_snap_func = functools.partial(self.client.create_snap,
|
||||
src_lun_id, src_snap_name)
|
||||
with utils.assure_cleanup(create_snap_func,
|
||||
self.client.delete_snap,
|
||||
True) as src_snap:
|
||||
LOG.debug('Internal snapshot for clone is created, '
|
||||
'name: %(name)s, id: %(id)s.',
|
||||
{'name': src_snap_name,
|
||||
'id': src_snap.get_id()})
|
||||
return self._create_volume_from_snap(
|
||||
volume, src_snap, size_in_m=utils.gib_to_mib(volume.size))
|
||||
|
||||
def get_pool_name(self, volume):
|
||||
return self.client.get_pool_name(volume.name)
|
||||
|
||||
|
||||
class ISCSIAdapter(CommonAdapter):
|
||||
protocol = PROTOCOL_ISCSI
|
||||
driver_name = 'UnityISCSIDriver'
|
||||
driver_volume_type = 'iscsi'
|
||||
|
||||
def get_connector_uids(self, connector):
|
||||
return utils.extract_iscsi_uids(connector)
|
||||
|
||||
def get_connection_info(self, hlu, host, connector):
|
||||
targets = self.client.get_iscsi_target_info()
|
||||
if not targets:
|
||||
msg = _("There is no accessible iSCSI targets on the system.")
|
||||
raise exception.VolumeBackendAPIException(data=msg)
|
||||
one_target = random.choice(targets)
|
||||
portals = [a['portal'] for a in targets]
|
||||
iqns = [a['iqn'] for a in targets]
|
||||
data = {
|
||||
'target_luns': [hlu] * len(portals),
|
||||
'target_iqns': iqns,
|
||||
'target_portals': portals,
|
||||
'target_lun': hlu,
|
||||
'target_portal': one_target['portal'],
|
||||
'target_iqn': one_target['iqn'],
|
||||
}
|
||||
return data
|
||||
|
||||
|
||||
class FCAdapter(CommonAdapter):
|
||||
protocol = PROTOCOL_FC
|
||||
driver_name = 'UnityFCDriver'
|
||||
driver_volume_type = 'fibre_channel'
|
||||
|
||||
def __init__(self, version=None):
|
||||
super(FCAdapter, self).__init__(version=version)
|
||||
self.lookup_service = None
|
||||
|
||||
def do_setup(self, driver, config):
|
||||
super(FCAdapter, self).do_setup(driver, config)
|
||||
self.lookup_service = utils.create_lookup_service()
|
||||
|
||||
def get_connector_uids(self, connector):
|
||||
return utils.extract_fc_uids(connector)
|
||||
|
||||
@property
|
||||
def auto_zone_enabled(self):
|
||||
return self.lookup_service is not None
|
||||
|
||||
def get_connection_info(self, hlu, host, connector):
|
||||
targets = self.client.get_fc_target_info(
|
||||
host, logged_in_only=(not self.auto_zone_enabled))
|
||||
|
||||
if not targets:
|
||||
msg = _("There is no accessible fibre channel targets on the "
|
||||
"system.")
|
||||
raise exception.VolumeBackendAPIException(data=msg)
|
||||
|
||||
if self.auto_zone_enabled:
|
||||
data = self._get_fc_zone_info(connector['wwpns'], targets)
|
||||
else:
|
||||
data = {
|
||||
'target_wwn': targets,
|
||||
}
|
||||
data['target_lun'] = hlu
|
||||
return data
|
||||
|
||||
def terminate_connection(self, volume, connector):
|
||||
super(FCAdapter, self).terminate_connection(volume, connector)
|
||||
|
||||
ret = None
|
||||
if self.auto_zone_enabled:
|
||||
ret = {
|
||||
'driver_volume_type': self.driver_volume_type,
|
||||
'data': {}
|
||||
}
|
||||
host = self.client.get_host(connector['host'])
|
||||
if len(host.host_luns) == 0:
|
||||
targets = self.client.get_fc_target_info(logged_in_only=True)
|
||||
ret['data'] = self._get_fc_zone_info(connector['wwpns'],
|
||||
targets)
|
||||
|
||||
return ret
|
||||
|
||||
def _get_fc_zone_info(self, initiator_wwns, target_wwns):
|
||||
mapping = self.lookup_service.get_device_mapping_from_network(
|
||||
initiator_wwns, target_wwns)
|
||||
targets, itor_tgt_map = utils.convert_to_itor_tgt_map(mapping)
|
||||
return {
|
||||
'target_wwn': targets,
|
||||
'initiator_target_map': itor_tgt_map,
|
||||
}
|
279
cinder/volume/drivers/dell_emc/unity/client.py
Normal file
279
cinder/volume/drivers/dell_emc/unity/client.py
Normal file
@ -0,0 +1,279 @@
|
||||
# Copyright (c) 2016 Dell Inc. or its subsidiaries.
|
||||
# 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.
|
||||
|
||||
from oslo_log import log
|
||||
from oslo_utils import excutils
|
||||
from oslo_utils import importutils
|
||||
|
||||
storops = importutils.try_import('storops')
|
||||
if storops:
|
||||
from storops import exception as storops_ex
|
||||
else:
|
||||
# Set storops_ex to be None for unit test
|
||||
storops_ex = None
|
||||
|
||||
from cinder import exception
|
||||
from cinder.i18n import _, _LW
|
||||
from cinder.volume.drivers.dell_emc.unity import utils
|
||||
|
||||
|
||||
LOG = log.getLogger(__name__)
|
||||
|
||||
|
||||
class UnityClient(object):
|
||||
def __init__(self, host, username, password, verify_cert=True):
|
||||
if storops is None:
|
||||
msg = _('Python package storops is not installed which '
|
||||
'is required to run Unity driver.')
|
||||
raise exception.VolumeBackendAPIException(data=msg)
|
||||
self._system = None
|
||||
self.host = host
|
||||
self.username = username
|
||||
self.password = password
|
||||
self.verify_cert = verify_cert
|
||||
|
||||
@property
|
||||
def system(self):
|
||||
if self._system is None:
|
||||
self._system = storops.UnitySystem(
|
||||
host=self.host, username=self.username, password=self.password,
|
||||
verify=self.verify_cert)
|
||||
return self._system
|
||||
|
||||
def get_serial(self):
|
||||
return self.system.serial_number
|
||||
|
||||
def create_lun(self, name, size, pool, description=None,
|
||||
io_limit_policy=None):
|
||||
"""Creates LUN on the Unity system.
|
||||
|
||||
:param name: lun name
|
||||
:param size: lun size in GiB
|
||||
:param pool: UnityPool object represent to pool to place the lun
|
||||
:param description: lun description
|
||||
:param io_limit_policy: io limit on the LUN
|
||||
:return: UnityLun object
|
||||
"""
|
||||
try:
|
||||
lun = pool.create_lun(lun_name=name, size_gb=size,
|
||||
description=description,
|
||||
io_limit_policy=io_limit_policy)
|
||||
except storops_ex.UnityLunNameInUseError:
|
||||
LOG.debug("LUN %s already exists. Return the existing one.",
|
||||
name)
|
||||
lun = self.system.get_lun(name=name)
|
||||
return lun
|
||||
|
||||
def delete_lun(self, lun_id):
|
||||
"""Deletes LUN on the Unity system.
|
||||
|
||||
:param lun_id: id of the LUN
|
||||
"""
|
||||
try:
|
||||
lun = self.system.get_lun(_id=lun_id)
|
||||
lun.delete()
|
||||
except storops_ex.UnityResourceNotFoundError:
|
||||
LOG.debug("LUN %s doesn't exist. Deletion is not needed.",
|
||||
lun_id)
|
||||
|
||||
def get_lun(self, lun_id=None, name=None):
|
||||
"""Gets LUN on the Unity system.
|
||||
|
||||
:param lun_id: id of the LUN
|
||||
:param name: name of the LUN
|
||||
:return: `UnityLun` object
|
||||
"""
|
||||
lun = None
|
||||
if lun_id is None and name is None:
|
||||
LOG.warning(
|
||||
_LW("Both lun_id and name are None to get LUN. Return None."))
|
||||
else:
|
||||
try:
|
||||
lun = self.system.get_lun(_id=lun_id, name=name)
|
||||
except storops_ex.UnityResourceNotFoundError:
|
||||
LOG.warning(
|
||||
_LW("LUN id=%(id)s, name=%(name)s doesn't exist."),
|
||||
{'id': lun_id, 'name': name})
|
||||
return lun
|
||||
|
||||
def extend_lun(self, lun_id, size_gib):
|
||||
lun = self.system.get_lun(lun_id)
|
||||
try:
|
||||
lun.total_size_gb = size_gib
|
||||
except storops_ex.UnityNothingToModifyError:
|
||||
LOG.debug("LUN %s is already expanded. LUN expand is not needed.",
|
||||
lun_id)
|
||||
return lun
|
||||
|
||||
def get_pools(self):
|
||||
"""Gets all storage pools on the Unity system.
|
||||
|
||||
:return: list of UnityPool object
|
||||
"""
|
||||
return self.system.get_pool()
|
||||
|
||||
def create_snap(self, src_lun_id, name=None):
|
||||
"""Creates a snapshot of LUN on the Unity system.
|
||||
|
||||
:param src_lun_id: the source LUN ID of the snapshot.
|
||||
:param name: the name of the snapshot. The Unity system will give one
|
||||
if `name` is None.
|
||||
"""
|
||||
try:
|
||||
lun = self.get_lun(lun_id=src_lun_id)
|
||||
snap = lun.create_snap(name, is_auto_delete=False)
|
||||
except storops_ex.UnitySnapNameInUseError as err:
|
||||
LOG.debug(
|
||||
"Snap %(snap_name)s already exists on LUN %(lun_id)s. "
|
||||
"Return the existing one. Message: %(err)s",
|
||||
{'snap_name': name,
|
||||
'lun_id': src_lun_id,
|
||||
'err': err})
|
||||
snap = self.get_snap(name=name)
|
||||
return snap
|
||||
|
||||
@staticmethod
|
||||
def delete_snap(snap):
|
||||
if snap is None:
|
||||
LOG.debug("Snap to delete is None, skipping deletion.")
|
||||
return
|
||||
|
||||
try:
|
||||
snap.delete()
|
||||
except storops_ex.UnityResourceNotFoundError as err:
|
||||
LOG.debug("Snap %(snap_name)s may be deleted already. "
|
||||
"Message: %(err)s",
|
||||
{'snap_name': snap.name,
|
||||
'err': err})
|
||||
except storops_ex.UnityDeleteAttachedSnapError as err:
|
||||
with excutils.save_and_reraise_exception():
|
||||
LOG.warning(_LW("Failed to delete snapshot %(snap_name)s "
|
||||
"which is in use. Message: %(err)s"),
|
||||
{'snap_name': snap.name, 'err': err})
|
||||
|
||||
def get_snap(self, name=None):
|
||||
try:
|
||||
return self.system.get_snap(name=name)
|
||||
except storops_ex.UnityResourceNotFoundError as err:
|
||||
msg = _LW("Snapshot %(name)s doesn't exist. Message: %(err)s")
|
||||
LOG.warning(msg, {'name': name, 'err': err})
|
||||
return None
|
||||
|
||||
def create_host(self, name, uids):
|
||||
"""Creates a host on Unity.
|
||||
|
||||
Creates a host on Unity which has the uids associated.
|
||||
|
||||
:param name: name of the host
|
||||
:param uids: iqns or wwns list
|
||||
:return: UnitHost object
|
||||
"""
|
||||
|
||||
try:
|
||||
host = self.system.get_host(name=name)
|
||||
except storops_ex.UnityResourceNotFoundError:
|
||||
LOG.debug('Existing host %s not found. Create a new one.', name)
|
||||
host = self.system.create_host(name=name)
|
||||
|
||||
host_initiators_ids = self.get_host_initiator_ids(host)
|
||||
un_registered = filter(lambda h: h not in host_initiators_ids, uids)
|
||||
for uid in un_registered:
|
||||
host.add_initiator(uid, force_create=True)
|
||||
|
||||
host.update()
|
||||
return host
|
||||
|
||||
@staticmethod
|
||||
def get_host_initiator_ids(host):
|
||||
fc = host.fc_host_initiators
|
||||
fc_ids = [] if fc is None else fc.initiator_id
|
||||
iscsi = host.iscsi_host_initiators
|
||||
iscsi_ids = [] if iscsi is None else iscsi.initiator_id
|
||||
return fc_ids + iscsi_ids
|
||||
|
||||
@staticmethod
|
||||
def attach(host, lun_or_snap):
|
||||
"""Attaches a `UnityLun` or `UnitySnap` to a `UnityHost`.
|
||||
|
||||
:param host: `UnityHost` object
|
||||
:param lun_or_snap: `UnityLun` or `UnitySnap` object
|
||||
:return: hlu
|
||||
"""
|
||||
try:
|
||||
return host.attach(lun_or_snap, skip_hlu_0=True)
|
||||
except storops_ex.UnityResourceAlreadyAttachedError:
|
||||
return host.get_hlu(lun_or_snap)
|
||||
|
||||
@staticmethod
|
||||
def detach(host, lun_or_snap):
|
||||
"""Detaches a `UnityLun` or `UnitySnap` from a `UnityHost`.
|
||||
|
||||
:param host: `UnityHost` object
|
||||
:param lun_or_snap: `UnityLun` object
|
||||
"""
|
||||
lun_or_snap.update()
|
||||
host.detach(lun_or_snap)
|
||||
|
||||
def get_host(self, name):
|
||||
return self.system.get_host(name=name)
|
||||
|
||||
def get_iscsi_target_info(self):
|
||||
portals = self.system.get_iscsi_portal()
|
||||
return [{'portal': utils.convert_ip_to_portal(p.ip_address),
|
||||
'iqn': p.iscsi_node.name}
|
||||
for p in portals]
|
||||
|
||||
def get_fc_target_info(self, host=None, logged_in_only=False):
|
||||
"""Get the ports WWN of FC on array.
|
||||
|
||||
:param host: the host to which the FC port is registered.
|
||||
:param logged_in_only: whether to retrieve only the logged-in port.
|
||||
|
||||
:return the WWN of FC ports. For example, the FC WWN on array is like:
|
||||
50:06:01:60:89:20:09:25:50:06:01:6C:09:20:09:25.
|
||||
This function removes the colons and returns the last 16 bits:
|
||||
5006016C09200925.
|
||||
"""
|
||||
ports = []
|
||||
if logged_in_only:
|
||||
for host_initiator in host.fc_host_initiators:
|
||||
paths = host_initiator.paths or []
|
||||
for path in paths:
|
||||
if path.is_logged_in:
|
||||
ports.append(path.fc_port)
|
||||
else:
|
||||
ports = self.system.get_fc_port()
|
||||
return [po.wwn.replace(':', '')[16:] for po in ports]
|
||||
|
||||
def create_io_limit_policy(self, name, max_iops=None, max_kbps=None):
|
||||
try:
|
||||
limit = self.system.create_io_limit_policy(
|
||||
name, max_iops=max_iops, max_kbps=max_kbps)
|
||||
except storops_ex.UnityPolicyNameInUseError:
|
||||
limit = self.system.get_io_limit_policy(name=name)
|
||||
return limit
|
||||
|
||||
def get_io_limit_policy(self, qos_specs):
|
||||
limit_policy = None
|
||||
if qos_specs is not None:
|
||||
limit_policy = self.create_io_limit_policy(
|
||||
qos_specs['id'],
|
||||
qos_specs.get(utils.QOS_MAX_IOPS),
|
||||
qos_specs.get(utils.QOS_MAX_BWS))
|
||||
return limit_policy
|
||||
|
||||
def get_pool_name(self, lun_name):
|
||||
lun = self.system.get_lun(name=lun_name)
|
||||
return lun.pool_name
|
216
cinder/volume/drivers/dell_emc/unity/driver.py
Normal file
216
cinder/volume/drivers/dell_emc/unity/driver.py
Normal file
@ -0,0 +1,216 @@
|
||||
# Copyright (c) 2016 Dell Inc. or its subsidiaries.
|
||||
# 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.
|
||||
|
||||
"""Cinder Driver for Unity"""
|
||||
|
||||
from oslo_config import cfg
|
||||
from oslo_log import log as logging
|
||||
|
||||
from cinder import interface
|
||||
from cinder.volume import driver
|
||||
from cinder.volume.drivers.dell_emc.unity import adapter
|
||||
from cinder.volume.drivers.san.san import san_opts
|
||||
from cinder.zonemanager import utils as zm_utils
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
CONF = cfg.CONF
|
||||
|
||||
UNITY_OPTS = [
|
||||
cfg.ListOpt('unity_storage_pool_names',
|
||||
default=None,
|
||||
help='A comma-separated list of storage pool names '
|
||||
'to be used.')]
|
||||
|
||||
CONF.register_opts(UNITY_OPTS)
|
||||
|
||||
|
||||
@interface.volumedriver
|
||||
class UnityDriver(driver.TransferVD,
|
||||
driver.ManageableVD,
|
||||
driver.ExtendVD,
|
||||
driver.SnapshotVD,
|
||||
driver.ManageableSnapshotsVD,
|
||||
driver.BaseVD):
|
||||
"""Unity Driver.
|
||||
|
||||
Version history:
|
||||
1.0.0 - Initial version
|
||||
"""
|
||||
|
||||
VERSION = '01.00.00'
|
||||
VENDOR = 'Dell EMC'
|
||||
# ThirdPartySystems wiki page
|
||||
CI_WIKI_NAME = "EMC_UNITY_CI"
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(UnityDriver, self).__init__(*args, **kwargs)
|
||||
self.configuration.append_config_values(UNITY_OPTS)
|
||||
self.configuration.append_config_values(san_opts)
|
||||
protocol = self.configuration.storage_protocol
|
||||
if protocol.lower() == adapter.PROTOCOL_FC.lower():
|
||||
self.protocol = adapter.PROTOCOL_FC
|
||||
self.adapter = adapter.FCAdapter(self.VERSION)
|
||||
else:
|
||||
self.protocol = adapter.PROTOCOL_ISCSI
|
||||
self.adapter = adapter.ISCSIAdapter(self.VERSION)
|
||||
|
||||
def do_setup(self, context):
|
||||
self.adapter.do_setup(self, self.configuration)
|
||||
|
||||
def check_for_setup_error(self):
|
||||
pass
|
||||
|
||||
def create_volume(self, volume):
|
||||
"""Creates a volume."""
|
||||
return self.adapter.create_volume(volume)
|
||||
|
||||
def create_volume_from_snapshot(self, volume, snapshot):
|
||||
"""Creates a volume from a snapshot."""
|
||||
return self.adapter.create_volume_from_snapshot(volume, snapshot)
|
||||
|
||||
def create_cloned_volume(self, volume, src_vref):
|
||||
"""Creates a cloned volume."""
|
||||
return self.adapter.create_cloned_volume(volume, src_vref)
|
||||
|
||||
def extend_volume(self, volume, new_size):
|
||||
"""Extend a volume."""
|
||||
self.adapter.extend_volume(volume, new_size)
|
||||
|
||||
def delete_volume(self, volume):
|
||||
"""Deletes a volume."""
|
||||
self.adapter.delete_volume(volume)
|
||||
|
||||
def create_snapshot(self, snapshot):
|
||||
"""Creates a snapshot."""
|
||||
self.adapter.create_snapshot(snapshot)
|
||||
|
||||
def delete_snapshot(self, snapshot):
|
||||
"""Deletes a snapshot."""
|
||||
self.adapter.delete_snapshot(snapshot)
|
||||
|
||||
def ensure_export(self, context, volume):
|
||||
"""Driver entry point to get the export info for an existing volume."""
|
||||
pass
|
||||
|
||||
def create_export(self, context, volume, connector):
|
||||
"""Driver entry point to get the export info for a new volume."""
|
||||
pass
|
||||
|
||||
def remove_export(self, context, volume):
|
||||
"""Driver entry point to remove an export for a volume."""
|
||||
pass
|
||||
|
||||
def check_for_export(self, context, volume_id):
|
||||
"""Make sure volume is exported."""
|
||||
pass
|
||||
|
||||
@zm_utils.AddFCZone
|
||||
def initialize_connection(self, volume, connector):
|
||||
"""Initializes the connection and returns connection info.
|
||||
|
||||
Assign any created volume to a compute node/host so that it can be
|
||||
used from that host.
|
||||
|
||||
The driver returns a driver_volume_type of 'fibre_channel'.
|
||||
The target_wwn can be a single entry or a list of wwns that
|
||||
correspond to the list of remote wwn(s) that will export the volume.
|
||||
The initiator_target_map is a map that represents the remote wwn(s)
|
||||
and a list of wwns which are visible to the remote wwn(s).
|
||||
Example return values:
|
||||
FC:
|
||||
{
|
||||
'driver_volume_type': 'fibre_channel'
|
||||
'data': {
|
||||
'target_discovered': True,
|
||||
'target_lun': 1,
|
||||
'target_wwn': ['1234567890123', '0987654321321'],
|
||||
'initiator_target_map': {
|
||||
'1122334455667788': ['1234567890123',
|
||||
'0987654321321']
|
||||
}
|
||||
}
|
||||
}
|
||||
iSCSI:
|
||||
{
|
||||
'driver_volume_type': 'iscsi'
|
||||
'data': {
|
||||
'target_discovered': True,
|
||||
'target_iqns': ['iqn.2010-10.org.openstack:volume-00001',
|
||||
'iqn.2010-10.org.openstack:volume-00002'],
|
||||
'target_portals': ['127.0.0.1:3260', '127.0.1.1:3260'],
|
||||
'target_luns': [1, 1],
|
||||
}
|
||||
}
|
||||
"""
|
||||
LOG.debug("Entering initialize_connection"
|
||||
" - connector: %(connector)s.",
|
||||
{'connector': connector})
|
||||
conn_info = self.adapter.initialize_connection(volume,
|
||||
connector)
|
||||
LOG.debug("Exit initialize_connection"
|
||||
" - Returning connection info: %(conn_info)s.",
|
||||
{'conn_info': conn_info})
|
||||
return conn_info
|
||||
|
||||
@zm_utils.RemoveFCZone
|
||||
def terminate_connection(self, volume, connector, **kwargs):
|
||||
"""Disallow connection from connector."""
|
||||
LOG.debug("Entering terminate_connection"
|
||||
" - connector: %(connector)s.",
|
||||
{'connector': connector})
|
||||
conn_info = self.adapter.terminate_connection(volume, connector)
|
||||
LOG.debug("Exit terminate_connection"
|
||||
" - Returning connection info: %(conn_info)s.",
|
||||
{'conn_info': conn_info})
|
||||
return conn_info
|
||||
|
||||
def get_volume_stats(self, refresh=False):
|
||||
"""Get volume stats.
|
||||
|
||||
:param refresh: True to get updated data
|
||||
"""
|
||||
if refresh:
|
||||
self.update_volume_stats()
|
||||
|
||||
return self._stats
|
||||
|
||||
def update_volume_stats(self):
|
||||
"""Retrieve stats info from volume group."""
|
||||
LOG.debug("Updating volume stats.")
|
||||
stats = self.adapter.update_volume_stats()
|
||||
stats['driver_version'] = self.VERSION
|
||||
stats['vendor_name'] = self.VENDOR
|
||||
self._stats = stats
|
||||
|
||||
def manage_existing(self, volume, existing_ref):
|
||||
"""Manages an existing LUN in the array.
|
||||
|
||||
:param volume: the mapping cinder volume of the Unity LUN.
|
||||
:param existing_ref: the Unity LUN info.
|
||||
"""
|
||||
return self.adapter.manage_existing(volume, existing_ref)
|
||||
|
||||
def manage_existing_get_size(self, volume, existing_ref):
|
||||
"""Returns size of volume to be managed by manage_existing."""
|
||||
return self.adapter.manage_existing_get_size(volume, existing_ref)
|
||||
|
||||
def get_pool(self, volume):
|
||||
"""Returns the pool name of a volume."""
|
||||
return self.adapter.get_pool_name(volume)
|
||||
|
||||
def unmanage(self, volume):
|
||||
"""Unmanages a volume."""
|
||||
pass
|
263
cinder/volume/drivers/dell_emc/unity/utils.py
Normal file
263
cinder/volume/drivers/dell_emc/unity/utils.py
Normal file
@ -0,0 +1,263 @@
|
||||
# Copyright (c) 2016 Dell Inc. or its subsidiaries.
|
||||
# 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.
|
||||
|
||||
from __future__ import division
|
||||
|
||||
import contextlib
|
||||
import functools
|
||||
from oslo_log import log as logging
|
||||
from oslo_utils import units
|
||||
import six
|
||||
|
||||
from cinder import exception
|
||||
from cinder.i18n import _, _LW
|
||||
from cinder.volume import utils as vol_utils
|
||||
from cinder.volume import volume_types
|
||||
from cinder.zonemanager import utils as zm_utils
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
BACKEND_QOS_CONSUMERS = frozenset(['back-end', 'both'])
|
||||
QOS_MAX_IOPS = 'maxIOPS'
|
||||
QOS_MAX_BWS = 'maxBWS'
|
||||
|
||||
|
||||
def dump_provider_location(location_dict):
|
||||
sorted_keys = sorted(location_dict.keys())
|
||||
return '|'.join('%(k)s^%(v)s' % {'k': k, 'v': location_dict[k]}
|
||||
for k in sorted_keys)
|
||||
|
||||
|
||||
def build_provider_location(system, lun_type, lun_id, version):
|
||||
"""Builds provider_location for volume or snapshot.
|
||||
|
||||
:param system: Unity serial number
|
||||
:param lun_id: LUN ID in Unity
|
||||
:param lun_type: 'lun'
|
||||
:param version: driver version
|
||||
"""
|
||||
location_dict = {'system': system,
|
||||
'type': lun_type,
|
||||
'id': six.text_type(lun_id),
|
||||
'version': version}
|
||||
return dump_provider_location(location_dict)
|
||||
|
||||
|
||||
def extract_provider_location(provider_location, key):
|
||||
"""Extracts value of the specified field from provider_location string.
|
||||
|
||||
:param provider_location: provider_location string
|
||||
:param key: field name of the value that to be extracted
|
||||
:return: value of the specified field if it exists, otherwise,
|
||||
None is returned
|
||||
"""
|
||||
if provider_location:
|
||||
for kvp in provider_location.split('|'):
|
||||
fields = kvp.split('^')
|
||||
if len(fields) == 2 and fields[0] == key:
|
||||
return fields[1]
|
||||
else:
|
||||
msg = _LW('"%(key)s" is not found in provider '
|
||||
'location "%(location)s."')
|
||||
LOG.warning(msg, {'key': key, 'location': provider_location})
|
||||
else:
|
||||
LOG.warning(_LW('Empty provider location received.'))
|
||||
|
||||
|
||||
def byte_to_gib(byte):
|
||||
return byte / units.Gi
|
||||
|
||||
|
||||
def byte_to_mib(byte):
|
||||
return byte / units.Mi
|
||||
|
||||
|
||||
def gib_to_mib(gib):
|
||||
return gib * units.Ki
|
||||
|
||||
|
||||
def validate_pool_names(conf_pools, array_pools):
|
||||
if not conf_pools:
|
||||
LOG.debug('No storage pools are specified. This host will manage '
|
||||
'all the pools on the Unity system.')
|
||||
return array_pools
|
||||
|
||||
conf_pools = set(map(lambda i: i.strip(), conf_pools))
|
||||
array_pools = set(map(lambda i: i.strip(), array_pools))
|
||||
existed = conf_pools & array_pools
|
||||
|
||||
if not existed:
|
||||
msg = (_('No storage pools to be managed exist. Please check '
|
||||
'your configuration. The available storage pools on the '
|
||||
'system are %s.') % array_pools)
|
||||
raise exception.VolumeBackendAPIException(data=msg)
|
||||
|
||||
return existed
|
||||
|
||||
|
||||
def extract_iscsi_uids(connector):
|
||||
if 'initiator' not in connector:
|
||||
msg = _("Host %s doesn't have iSCSI initiator.") % connector['host']
|
||||
raise exception.VolumeBackendAPIException(data=msg)
|
||||
|
||||
return [connector['initiator']]
|
||||
|
||||
|
||||
def extract_fc_uids(connector):
|
||||
if 'wwnns' not in connector or 'wwpns' not in connector:
|
||||
msg = _("Host %s doesn't have FC initiators.") % connector['host']
|
||||
LOG.error(msg)
|
||||
raise exception.VolumeBackendAPIException(data=msg)
|
||||
|
||||
wwnns = connector['wwnns']
|
||||
wwpns = connector['wwpns']
|
||||
wwns = [(node + port).upper() for node, port in zip(wwnns, wwpns)]
|
||||
|
||||
def _to_wwn(wwn):
|
||||
# Format the wwn to include the colon
|
||||
# For example, convert 1122200000051E55E100 to
|
||||
# 11:22:20:00:00:05:1E:55:A1:00
|
||||
return ':'.join(wwn[i:i + 2] for i in range(0, len(wwn), 2))
|
||||
|
||||
return list(map(_to_wwn, wwns))
|
||||
|
||||
|
||||
def convert_ip_to_portal(ip):
|
||||
return '%s:3260' % ip
|
||||
|
||||
|
||||
def convert_to_itor_tgt_map(zone_mapping):
|
||||
"""Function to process data from lookup service.
|
||||
|
||||
:param zone_mapping: mapping is the data from the zone lookup service
|
||||
with below format
|
||||
{
|
||||
<San name>: {
|
||||
'initiator_port_wwn_list':
|
||||
('200000051e55a100', '200000051e55a121'..)
|
||||
'target_port_wwn_list':
|
||||
('100000051e55a100', '100000051e55a121'..)
|
||||
}
|
||||
}
|
||||
"""
|
||||
target_wwns = []
|
||||
itor_tgt_map = {}
|
||||
for san_name in zone_mapping:
|
||||
one_map = zone_mapping[san_name]
|
||||
for target in one_map['target_port_wwn_list']:
|
||||
if target not in target_wwns:
|
||||
target_wwns.append(target)
|
||||
for initiator in one_map['initiator_port_wwn_list']:
|
||||
itor_tgt_map[initiator] = one_map['target_port_wwn_list']
|
||||
LOG.debug("target_wwns: %(tgt_wwns)s\n init_targ_map: %(itor_tgt_map)s",
|
||||
{'tgt_wwns': target_wwns,
|
||||
'itor_tgt_map': itor_tgt_map})
|
||||
return target_wwns, itor_tgt_map
|
||||
|
||||
|
||||
def get_pool_name(volume):
|
||||
return vol_utils.extract_host(volume.host, 'pool')
|
||||
|
||||
|
||||
def get_extra_spec(volume, spec_key):
|
||||
spec_value = None
|
||||
type_id = volume.volume_type_id
|
||||
if type_id is not None:
|
||||
extra_specs = volume_types.get_volume_type_extra_specs(type_id)
|
||||
if spec_key in extra_specs:
|
||||
spec_value = extra_specs[spec_key]
|
||||
return spec_value
|
||||
|
||||
|
||||
def ignore_exception(func, *args, **kwargs):
|
||||
try:
|
||||
func(*args, **kwargs)
|
||||
except Exception as ex:
|
||||
LOG.warning(_LW('Error occurred but ignored. Function: %(func_name)s, '
|
||||
'args: %(args)s, kwargs: %(kwargs)s, '
|
||||
'exception: %(ex)s.'),
|
||||
{'func_name': func, 'args': args,
|
||||
'kwargs': kwargs, 'ex': ex})
|
||||
|
||||
|
||||
@contextlib.contextmanager
|
||||
def assure_cleanup(enter_func, exit_func, use_enter_return):
|
||||
"""Assures the resource is cleaned up. Used as a context.
|
||||
|
||||
:param enter_func: the function to execute when entering the context.
|
||||
:param exit_func: the function to execute when leaving the context.
|
||||
:param use_enter_return: the flag indicates whether to pass the return
|
||||
value of enter_func in to the exit_func.
|
||||
"""
|
||||
|
||||
enter_return = None
|
||||
try:
|
||||
if isinstance(enter_func, functools.partial):
|
||||
enter_func_name = enter_func.func.__name__
|
||||
else:
|
||||
enter_func_name = enter_func.__name__
|
||||
LOG.debug(('Entering context. Function: %(func_name)s, '
|
||||
'use_enter_return: %(use)s.'),
|
||||
{'func_name': enter_func_name,
|
||||
'use': use_enter_return})
|
||||
enter_return = enter_func()
|
||||
yield enter_return
|
||||
finally:
|
||||
if isinstance(exit_func, functools.partial):
|
||||
exit_func_name = exit_func.func.__name__
|
||||
else:
|
||||
exit_func_name = exit_func.__name__
|
||||
LOG.debug(('Exiting context. Function: %(func_name)s, '
|
||||
'use_enter_return: %(use)s.'),
|
||||
{'func_name': exit_func_name,
|
||||
'use': use_enter_return})
|
||||
if enter_return is not None:
|
||||
if use_enter_return:
|
||||
ignore_exception(exit_func, enter_return)
|
||||
else:
|
||||
ignore_exception(exit_func)
|
||||
|
||||
|
||||
def create_lookup_service():
|
||||
return zm_utils.create_lookup_service()
|
||||
|
||||
|
||||
def get_backend_qos_specs(volume):
|
||||
type_id = volume.volume_type_id
|
||||
if type_id is None:
|
||||
return None
|
||||
|
||||
qos_specs = volume_types.get_volume_type_qos_specs(type_id)
|
||||
if qos_specs is None:
|
||||
return None
|
||||
|
||||
qos_specs = qos_specs['qos_specs']
|
||||
if qos_specs is None:
|
||||
return None
|
||||
|
||||
consumer = qos_specs['consumer']
|
||||
# Front end QoS specs are handled by nova. We ignore them here.
|
||||
if consumer not in BACKEND_QOS_CONSUMERS:
|
||||
return None
|
||||
|
||||
max_iops = qos_specs['specs'].get(QOS_MAX_IOPS)
|
||||
max_bws = qos_specs['specs'].get(QOS_MAX_BWS)
|
||||
if max_iops is None and max_bws is None:
|
||||
return None
|
||||
|
||||
return {
|
||||
'id': qos_specs['id'],
|
||||
QOS_MAX_IOPS: max_iops,
|
||||
QOS_MAX_BWS: max_bws,
|
||||
}
|
@ -0,0 +1,3 @@
|
||||
---
|
||||
features:
|
||||
- Added backend driver for Dell EMC Unity storage.
|
Loading…
Reference in New Issue
Block a user