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:
Tina 2016-11-11 02:50:43 +00:00 committed by Tina Tang
parent a6b5a19e0d
commit 5a8f26eb62
13 changed files with 2812 additions and 0 deletions

View File

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

View File

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

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

View 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'))

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

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

View 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

View 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,
}

View 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

View 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

View 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,
}

View File

@ -0,0 +1,3 @@
---
features:
- Added backend driver for Dell EMC Unity storage.